mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-27 14:57:15 +00:00
16aaaf5450
* feat(collections): awaits_human collection setting (TASK-3919) A collection whose items are asks of a person (IDEA-3906): the needs-you router will treat an open, assigned item in it as "asked". Stored as settings.awaits_human like tracks_work: absent means false, a settings write that omits it keeps the stored value, a typed member on create and update sets it alone, and a non-boolean is refused 400. No migration (A3 default). - models: CollectionAwaitsHuman[JSON], Carry/Validate/Set, on shared bool setting helpers that the tracks_work functions now use too - store: create and update apply it; update carries it under the row lock - CLI: pad collection update --awaits-human[=false], checked on the response - web: "Items are asks of a person" toggle on the editor's Display tab - docs/api.md Claude-Session: https://claude.ai/code/session_01EJctmVZwefvYhqSRSJhNKX * fix(collections): read awaits_human per key, not through CollectionSettings (TASK-3919) A plain bool field made a stored non-boolean awaits_human (an import is not validated) fail the whole CollectionSettings unmarshal, so a reference collection read as work. CollectionAwaitsHumanJSON now reads the one key. Also corrects the --awaits-human comment: the flag shape is not MCP-safe. Claude-Session: https://claude.ai/code/session_01EJctmVZwefvYhqSRSJhNKX * test(store): assert the returned settings after turning awaits_human off (TASK-3919) staticcheck SA4006: the UpdateCollection result was assigned and never read. Claude-Session: https://claude.ai/code/session_01Fbkk7f7M5JqZE1Gc4ZSSYj * fix(cli): pad collection create takes --awaits-human (TASK-3919) Codex r2 P2: only `collection update` had the flag, so marking a human-tasks collection took a second call. create now sends the typed member and, like update, refuses to report success when an older server ignored it. Claude-Session: https://claude.ai/code/session_01EZ6hZzQiytCeBxXrtCWcVd
131 lines
4.7 KiB
Go
131 lines
4.7 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
)
|
|
|
|
// PLAN-3535: a collection either tracks WORK (its items have a lifecycle and
|
|
// finish) or holds REFERENCE material (its items just exist: docs,
|
|
// conventions, playbooks). A reference collection's items do not count toward
|
|
// parent progress, do not block a parent from closing, and stay out of
|
|
// Insights and the agent/dashboard open-work counts. They still count in the
|
|
// sidebar, search, links and their own collection list.
|
|
//
|
|
// Stored as CollectionSettings.TracksWork (JSON `tracks_work`). ABSENT means
|
|
// work, today's behaviour, so an old row, an API writer that never sends the
|
|
// key, and a client/server skew all keep counting as before. Every reader
|
|
// goes through CollectionTracksWork / CollectionTracksWorkJSON; nothing reads
|
|
// the key itself.
|
|
|
|
// CollectionTracksWork reports whether a collection's items count as work.
|
|
func CollectionTracksWork(settings CollectionSettings) bool {
|
|
return settings.TracksWork == nil || *settings.TracksWork
|
|
}
|
|
|
|
// CollectionTracksWorkJSON is CollectionTracksWork over the stored settings
|
|
// JSON. Unparseable settings count as work (today's behaviour).
|
|
func CollectionTracksWorkJSON(settingsJSON string) bool {
|
|
if strings.TrimSpace(settingsJSON) == "" {
|
|
return true
|
|
}
|
|
var s CollectionSettings
|
|
if err := json.Unmarshal([]byte(settingsJSON), &s); err != nil {
|
|
return true
|
|
}
|
|
return CollectionTracksWork(s)
|
|
}
|
|
|
|
// DefaultTracksWork is the value a NEW collection gets when its creator did
|
|
// not say: a system collection (Conventions, Playbooks) is reference; any
|
|
// other collection is work when its items can FINISH, that is, when its done
|
|
// field is a select and one of its options is terminal. Terminal means what
|
|
// every lifecycle reader means (TerminalValuesForDoneField): the declared
|
|
// terminal_options, or DefaultTerminalStatuses when the field declares none,
|
|
// so `status: open, done` with no terminal_options is work. Otherwise
|
|
// reference.
|
|
func DefaultTracksWork(schemaJSON, settingsJSON string, isSystem bool) bool {
|
|
if isSystem {
|
|
return false
|
|
}
|
|
var schema CollectionSchema
|
|
if err := UnmarshalItemFieldSchema([]byte(schemaJSON), &schema); err != nil {
|
|
return true
|
|
}
|
|
var settings CollectionSettings
|
|
_ = json.Unmarshal([]byte(settingsJSON), &settings)
|
|
key, terminals := TerminalValuesForDoneField(schema, settings)
|
|
for _, f := range schema.Fields {
|
|
if f.Key != key || f.Type != "select" {
|
|
continue
|
|
}
|
|
for _, opt := range f.Options {
|
|
for _, t := range terminals {
|
|
if strings.EqualFold(opt, t) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
// WithTracksWorkDefault returns settingsJSON with `tracks_work` set to
|
|
// DefaultTracksWork when the creator did not set it; settings that already
|
|
// carry the key, or that do not parse as an object, are returned unchanged.
|
|
// Used on collection CREATE only (not import, which keeps what it carries).
|
|
func WithTracksWorkDefault(settingsJSON, schemaJSON string, isSystem bool) string {
|
|
raw := strings.TrimSpace(settingsJSON)
|
|
if raw == "" {
|
|
raw = "{}"
|
|
}
|
|
var m map[string]json.RawMessage
|
|
if err := json.Unmarshal([]byte(raw), &m); err != nil || m == nil {
|
|
return settingsJSON
|
|
}
|
|
if _, ok := m["tracks_work"]; ok {
|
|
return settingsJSON
|
|
}
|
|
v := "false"
|
|
if DefaultTracksWork(schemaJSON, raw, isSystem) {
|
|
v = "true"
|
|
}
|
|
m["tracks_work"] = json.RawMessage(v)
|
|
out, err := json.Marshal(m)
|
|
if err != nil {
|
|
return settingsJSON
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
// CarryTracksWork returns newSettingsJSON with the stored `tracks_work` value
|
|
// carried over when the new settings do not mention the key (PLAN-3535).
|
|
// Settings are written wholesale, and every client written before the key
|
|
// existed rebuilds them without it, so absence on UPDATE means "not
|
|
// mentioned", never "reset to work". A new value that does set the key wins.
|
|
// Anything that does not parse as an object is returned unchanged.
|
|
func CarryTracksWork(newSettingsJSON, storedSettingsJSON string) string {
|
|
return carryBoolSetting(newSettingsJSON, storedSettingsJSON, "tracks_work")
|
|
}
|
|
|
|
// ValidateTracksWorkSetting refuses a `tracks_work` that is present but not a
|
|
// JSON boolean. An absent key is fine (it means work on create, "unchanged"
|
|
// on update).
|
|
func ValidateTracksWorkSetting(settingsJSON string) error {
|
|
if !boolSettingValid(settingsJSON, "tracks_work") {
|
|
return errTracksWorkNotBool
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var errTracksWorkNotBool = errors.New("settings.tracks_work must be true or false")
|
|
|
|
// SetTracksWork returns settingsJSON with tracks_work set to v and every other
|
|
// key unchanged. Settings that do not parse as an object are an error: the
|
|
// caller asked for a value it cannot store.
|
|
func SetTracksWork(settingsJSON string, v bool) (string, error) {
|
|
return setBoolSetting(settingsJSON, "tracks_work", v)
|
|
}
|