@arjunkhera/atlas 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +15 -9
- package/agents/artifact-renderer.md +7 -2
- package/agents/capability-reader.md +1 -1
- package/agents/code-explorer.md +3 -2
- package/agents/reviewer-architect.md +3 -3
- package/agents/reviewer-pm.md +4 -4
- package/agents/reviewer-security.md +11 -8
- package/agents/verifier.md +97 -0
- package/door/cli.mjs +33 -108
- package/door/lib/kind-drift.mjs +16 -6
- package/door/lib/privacy.mjs +61 -0
- package/door/lib/pull-request.mjs +55 -56
- package/door/lib/ste-words.json +51 -0
- package/door/lib/ste.mjs +199 -0
- package/door/lib/tooling.mjs +2 -2
- package/package.json +1 -1
- package/shape/check.mjs +33 -33
- package/shape/releases.json +5 -0
- package/skills/feedback/SKILL.md +39 -0
- package/skills/lead/SKILL.md +103 -0
- package/skills/learnings/SKILL.md +48 -0
- package/skills/repo-skills/SKILL.md +64 -0
- package/skills/sdlc-task/SKILL.md +62 -69
- package/skills/sdlc-task/lifecycle.yaml +8 -4
- package/skills/sdlc-task/templates/design-doc.md +23 -18
- package/skills/sdlc-task/templates/resume-anchor.md +9 -7
- package/work/lib/capability.mjs +34 -23
- package/work/lib/delivery.mjs +26 -12
- package/work/lib/questions.mjs +54 -10
- package/work/lib/verb-fields.mjs +34 -0
- package/work/lib/verbs.mjs +100 -19
- package/work/{manifest-0.5.0.json → manifest-0.6.0.json} +9 -3
- package/work/mcp.mjs +13 -10
- package/work/protected-paths.json +4 -2
- package/door/lib/helpers.mjs +0 -139
- package/door/lib/onboarding.mjs +0 -130
|
@@ -30,27 +30,27 @@ checks and the transition log; each verb's own description says what it needs.
|
|
|
30
30
|
| Record new work, with the owner's words | `item_propose`, then `item_stage` as it moves |
|
|
31
31
|
| Freeze the design | `item_lock` |
|
|
32
32
|
| Park it, or pick it up again | `item_park`, `item_resume` |
|
|
33
|
+
| Drop it, in the owner's words (`item_resume` undoes a drop) | `item_drop` |
|
|
34
|
+
| Keep its next step, its wait or its title true | `item_edit` |
|
|
33
35
|
| Split it, or link it to other work | `item_split`, `item_link` |
|
|
34
36
|
| Ask the owner something, or record the answer | `question_ask`, `question_answer` |
|
|
35
37
|
| Record a decision in the owner's words | `decision_record` |
|
|
36
38
|
| Mark it delivered, or reopen it | `item_done`, `item_reopen` |
|
|
37
39
|
| Say where things stand | `where_are_we`, `in_flight`, `needs_me` |
|
|
38
40
|
|
|
39
|
-
The repo's own facts come from `atlas.yaml` and `atlas/`. Its
|
|
40
|
-
`library`, `schema`)
|
|
41
|
-
Read
|
|
41
|
+
The repo's own facts come from `atlas.yaml` and `atlas/`. Its kinds (`service`,
|
|
42
|
+
`library`, `schema`, `cli`, `plugin`) decide which procedures it has and what
|
|
43
|
+
"delivered" means. Read them. Never assume a deployed service.
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
How to talk with the owner lives in the `atlas:lead` skill: one question at a
|
|
46
|
+
time, a page first with a worked example, plain words, push back. This skill
|
|
47
|
+
does not repeat those rules; follow the lead.
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
amend.
|
|
52
|
-
|
|
53
|
-
(Source: the Atlas lock procedure, amendment A1.6, section 12.)
|
|
49
|
+
**A change after the owner approved a design** is not a new lock. Say so at
|
|
50
|
+
once, and never build against a sentence you know is wrong. Show the owner the
|
|
51
|
+
change in one screen. When the owner approves it, record the owner's words with
|
|
52
|
+
`decision_record` on the same item, and write the change into the design file
|
|
53
|
+
with the date. (Target design, flow 4: long frozen texts with hashes retire.)
|
|
54
54
|
|
|
55
55
|
## start
|
|
56
56
|
|
|
@@ -68,73 +68,62 @@ frozen text, and the rule for one is short:
|
|
|
68
68
|
[`templates/design-doc.md`](templates/design-doc.md) to
|
|
69
69
|
`docs/design-docs/<slug>.md`, fill Context and goal, and list it in
|
|
70
70
|
`docs/index.md` in the commit that first lands it. Hotfix tier skips the
|
|
71
|
-
doc: the
|
|
71
|
+
doc: the definition of done lives on the work item, and a resume anchor
|
|
72
72
|
covers pauses.
|
|
73
73
|
5. Enter the design loop.
|
|
74
74
|
|
|
75
75
|
## The design loop (between start and lock)
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
surfacing question lists.
|
|
80
|
-
- **Guided elicitation** (the owner's words: *"one on one
|
|
81
|
-
conversation that flows… the purpose of agent is to do research and guide my
|
|
82
|
-
thoughts into useful features"*). Research first, then **one pointed question
|
|
83
|
-
at a time**, with a recommendation (the owner: *"Questions if posed
|
|
84
|
-
should be one by one. Just think, if you say 5 things in one go, how do I
|
|
85
|
-
respond."*). Keep the question queue in the design doc; surface only the
|
|
86
|
-
next one. Record each question with `question_ask` and each answer with
|
|
87
|
-
`question_answer`.
|
|
88
|
-
- **Succinct chat; artifacts carry the content** (the owner's words:
|
|
89
|
-
*"the content is very verbose… I won't read that much"* · *"artefacts should
|
|
90
|
-
be the default method of communication… use as many diagrams and examples as
|
|
91
|
-
possible"*). Chat is a few short sentences: the outcome, the artifact link,
|
|
92
|
-
the one question.
|
|
93
|
-
- **Lead with a worked example.** Real input, real process, real output, and a
|
|
94
|
-
sequence diagram where order matters. The owner cannot read dense prose.
|
|
95
|
-
- **Read the reason, not only the rule.** Before you build to a done line, read
|
|
96
|
-
the owner's words behind it and ask whether the thing should exist at all. A
|
|
97
|
-
done line is a measure, not the goal.
|
|
77
|
+
Talk to the owner by the `atlas:lead` skill. What this loop adds:
|
|
78
|
+
|
|
98
79
|
- **Research before opinions.** Fan out `atlas:code-explorer` for how the system
|
|
99
|
-
works today. Raw exploration never enters this session
|
|
80
|
+
works today. Raw exploration never enters this session; digests only.
|
|
81
|
+
- **Read the reason, not only the rule.** Before you build to a definition of
|
|
82
|
+
done, read the owner's words behind it and ask whether the thing should exist
|
|
83
|
+
at all.
|
|
100
84
|
- **When the task is a cluster of filed bugs, first ask whether the model
|
|
101
85
|
they presume was ever decided.** A bug report says the code surprised
|
|
102
86
|
someone; it does not say what the code should do.
|
|
87
|
+
- **User stories before the definition of done.** Never write checks for a
|
|
88
|
+
flow no story walks through.
|
|
103
89
|
- **The doc accretes in real time.** Every resolved question, decision and
|
|
104
|
-
approach lands in the design doc as it happens.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
*"I need to see how a user would use the product, or like how the flow
|
|
113
|
-
works"*). Never draft criteria for a flow no story walks through.
|
|
114
|
-
- **Adversarial review before lock.** Standard tier: at least one persona.
|
|
115
|
-
Initiative: the panel — `atlas:reviewer-pm`, `atlas:reviewer-architect`,
|
|
116
|
-
`atlas:reviewer-security` — each briefed with the doc path, in parallel. Brief each
|
|
117
|
-
one to ask whether the design is the right thing, not only whether it is
|
|
118
|
-
consistent. An unresolved finding goes into the doc's open questions, and
|
|
119
|
-
that blocks the lock.
|
|
90
|
+
approach lands in the design doc as it happens. Record each question with
|
|
91
|
+
`question_ask`, each answer with `question_answer`, each decision with
|
|
92
|
+
`decision_record`.
|
|
93
|
+
- **Adversarial review before the owner approves.** Standard tier: at least one
|
|
94
|
+
persona. Initiative: the panel, `atlas:reviewer-pm`, `atlas:reviewer-architect`
|
|
95
|
+
and `atlas:reviewer-security`, each briefed with the doc path, in parallel.
|
|
96
|
+
Brief each one to ask whether the design is the right thing. An unresolved
|
|
97
|
+
finding goes into the doc's open questions.
|
|
120
98
|
|
|
121
99
|
## lock
|
|
122
100
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
is
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
`
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
101
|
+
A lock is the owner's approval of a short design (target design, flow 4).
|
|
102
|
+
|
|
103
|
+
1. **The design is short and sized to the work.** A feature gets a full
|
|
104
|
+
document; a small fix gets a one-screen card. It holds what
|
|
105
|
+
`lifecycle.yaml` `lock.holds` lists: the definition of done, written as checks; the kind of change, its impact and its undo; and
|
|
106
|
+
each existing test it expects to change. A change that cannot be undone
|
|
107
|
+
shows in bold at the top.
|
|
108
|
+
2. **The owner approves it in the owner's own words.** Gate on
|
|
109
|
+
`lifecycle.yaml` `lock.preconditions`. Record the words with
|
|
110
|
+
`decision_record`, and mark the design doc "Approved <date>", with the words.
|
|
111
|
+
3. Call `item_lock` with the definition of done as `scope.text` and the design
|
|
112
|
+
file as `scope.design_doc`. The verb keeps a fingerprint of that text for
|
|
113
|
+
the tools. No person reads or writes a hash, and no frozen text is copied
|
|
114
|
+
into the doc.
|
|
115
|
+
4. Build against the approved design. **Tripwires** (`lifecycle.yaml`
|
|
116
|
+
`tripwires`): a discovery that touches the definition of done, security, a
|
|
117
|
+
schema or cost stops the build and goes back to the owner, as a change after
|
|
118
|
+
approval (above).
|
|
119
|
+
5. Test with the repo's own `test` procedure, which `atlas.yaml` names. Open a
|
|
120
|
+
pull request. Its text holds the definition of done, the kind of change, the
|
|
121
|
+
undo, and the list of existing tests it changes or removes, and it links the
|
|
122
|
+
tracker item.
|
|
123
|
+
6. **Proof before the merge.** Run the `atlas:verifier` agent on the pull
|
|
124
|
+
request. It proves each line of the definition of done from a fresh run and
|
|
125
|
+
posts its own proof comment. Never edit that comment. Ask the owner for the
|
|
126
|
+
merge only after the proof is there, and show the proof first.
|
|
138
127
|
|
|
139
128
|
## pause
|
|
140
129
|
|
|
@@ -142,6 +131,9 @@ is confirmed. Then:
|
|
|
142
131
|
resume anchor from [`templates/resume-anchor.md`](templates/resume-anchor.md)
|
|
143
132
|
(hotfix). The bar: a fresh session needs nothing else.
|
|
144
133
|
2. Call `item_park` with the reason. A parked item keeps its stage.
|
|
134
|
+
3. Before the session ends, keep each item you touched true: call
|
|
135
|
+
`item_edit` with its next step and what it waits on. If the owner retires
|
|
136
|
+
a piece of work, call `item_drop` with his words.
|
|
145
137
|
|
|
146
138
|
## resume
|
|
147
139
|
|
|
@@ -160,8 +152,9 @@ next action. One short paragraph.
|
|
|
160
152
|
## done
|
|
161
153
|
|
|
162
154
|
1. Prove delivery the way the repo's `kind` requires, then call `item_done`. A
|
|
163
|
-
`library`
|
|
164
|
-
|
|
155
|
+
`library` repo proves it by merged pull requests and a green gate. A
|
|
156
|
+
`service` or `schema` repo also needs its live checks, because a schema
|
|
157
|
+
migration reaches the running node after the merge.
|
|
165
158
|
2. If anything was a struggle, encode the fix where it stops a repeat — a lint,
|
|
166
159
|
then a tool, then a skill, then a doc — and say which.
|
|
167
160
|
3. Design doc status: `SHIPPED <date>`.
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
#
|
|
3
3
|
# Atlas is the package that sets how a repo is worked on, and this is its
|
|
4
4
|
# lifecycle. It travels with the package, beside the skill that reads it, not
|
|
5
|
-
# with any one repo.
|
|
5
|
+
# with any one repo. The skill reads these lists. The work verbs still keep
|
|
6
|
+
# their own copies (STAGES and LIVE_LIFECYCLES in work/lib), and the stage
|
|
7
|
+
# lists differ on ship; the register records that gap.
|
|
6
8
|
#
|
|
7
9
|
# The values came from the old ops pack, unchanged. That pack and every script
|
|
8
10
|
# that read it are deleted.
|
|
@@ -12,11 +14,13 @@ authority: the Atlas lock procedure, amendment A1.9, section 5, slice L2 stage o
|
|
|
12
14
|
lifecycle_states: [proposed, active, paused, done, dropped, archived]
|
|
13
15
|
stages: [capture, spec, build, verify, ship, learn]
|
|
14
16
|
|
|
17
|
+
# A lock is the owner's approval of a short design (target design, flow 4).
|
|
18
|
+
# Long frozen texts with hashes retired in stage 1.
|
|
15
19
|
lock:
|
|
16
|
-
|
|
17
|
-
preconditions: [open_questions_empty,
|
|
20
|
+
holds: [definition_of_done, kind_of_change, impact, undo, tests_it_changes]
|
|
21
|
+
preconditions: [open_questions_empty, owner_approved_in_own_words, approach_chosen, tier_confirmed]
|
|
18
22
|
|
|
19
|
-
tripwires: [
|
|
23
|
+
tripwires: [definition_of_done, security, schema_migrations, money_cost]
|
|
20
24
|
|
|
21
25
|
tiers: [hotfix, standard, initiative]
|
|
22
26
|
tier_escalators: [migrations, auth_rls_surface, new_external_dependency, money_cost]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- Living design document — template (
|
|
1
|
+
<!-- Living design document — template (sdlc-task skill, "The design loop").
|
|
2
2
|
Copy to docs/design-docs/<slug>.md at `sdlc-task start` (standard/initiative
|
|
3
3
|
tiers). Sections may be trimmed at standard tier; never at initiative.
|
|
4
4
|
The artifact projection republishes on change (D14: git is truth).
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
# <Feature name> — design
|
|
16
16
|
|
|
17
|
-
> Status: DRAFT (in design loop) |
|
|
17
|
+
> Status: DRAFT (in design loop) | APPROVED <date> | SHIPPED <date>
|
|
18
18
|
> Work item: <work item id> · Tier: hotfix | standard | initiative
|
|
19
19
|
> Artifact: <claude.ai artifact url once published>
|
|
20
20
|
|
|
@@ -46,7 +46,7 @@ and the one-line goal. Agent interpretation kept separate.>
|
|
|
46
46
|
what the system does, what they see — written as narrative the owner can
|
|
47
47
|
react to, each tagged to the slice that ships it. Illustrative dialogue
|
|
48
48
|
is fine; mark invented values as illustrative. Draft these BEFORE
|
|
49
|
-
|
|
49
|
+
the definition of done — its checks formalize what the stories show. -->
|
|
50
50
|
|
|
51
51
|
- **US-1 — <scene name>** *(slice)*: …
|
|
52
52
|
|
|
@@ -67,7 +67,7 @@ Standard tier: may collapse to "approach + rejected alternative, one line".>
|
|
|
67
67
|
## Open questions
|
|
68
68
|
|
|
69
69
|
<!-- The lock precondition: this checklist must be EMPTY (all items moved to
|
|
70
|
-
Resolved) before
|
|
70
|
+
Resolved) before the owner is asked to approve the design. -->
|
|
71
71
|
|
|
72
72
|
- [ ] OQ-1 —
|
|
73
73
|
|
|
@@ -77,7 +77,7 @@ Standard tier: may collapse to "approach + rejected alternative, one line".>
|
|
|
77
77
|
|
|
78
78
|
## Decision log
|
|
79
79
|
|
|
80
|
-
- D-1 — <decision> — <rationale> (<date>, <
|
|
80
|
+
- D-1 — <decision> — <rationale> (<date>, <who decided>)
|
|
81
81
|
|
|
82
82
|
## Risks & failure modes
|
|
83
83
|
|
|
@@ -89,29 +89,34 @@ Standard tier: may collapse to "approach + rejected alternative, one line".>
|
|
|
89
89
|
<!-- How each slice proves itself: suites, scenarios, eval questions, prod
|
|
90
90
|
verification. The red spec's table of contents. Trimmable at standard. -->
|
|
91
91
|
|
|
92
|
-
##
|
|
92
|
+
## Definition of done
|
|
93
93
|
|
|
94
|
-
<!--
|
|
95
|
-
|
|
94
|
+
<!-- Written as checks, before the build. The verifier proves each line from a
|
|
95
|
+
fresh run. Name each existing test this work expects to change. -->
|
|
96
96
|
|
|
97
97
|
1. …
|
|
98
98
|
|
|
99
|
-
##
|
|
99
|
+
## Kind of change, impact and undo
|
|
100
100
|
|
|
101
|
-
<!--
|
|
101
|
+
<!-- A change that cannot be undone goes in bold at the top of this doc. -->
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
- Kind: …
|
|
104
|
+
- Impact: …
|
|
105
|
+
- Undo: …
|
|
106
|
+
- Existing tests changed or removed: …
|
|
107
|
+
|
|
108
|
+
## Approval
|
|
109
|
+
|
|
110
|
+
<!-- The owner's approval of this short design is the lock (target design,
|
|
111
|
+
flow 4). Record the owner's words here and with decision_record. A change
|
|
112
|
+
after approval is a new approval, dated, in the same place. -->
|
|
113
|
+
|
|
114
|
+
Approved: <date>. The owner's words: "<words>".
|
|
110
115
|
|
|
111
116
|
## Deviation log (post-lock)
|
|
112
117
|
|
|
113
118
|
<!-- Every post-lock divergence from this document. Review checks this list.
|
|
114
|
-
A deviation touching a tripwire (
|
|
119
|
+
A deviation touching a tripwire (definition of done, security, schema, cost) must halt
|
|
115
120
|
and return to the owner instead of landing here. -->
|
|
116
121
|
|
|
117
122
|
| # | What changed vs the doc | Why | Tripwire? | Reviewed |
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
<!-- Resume anchor — template (
|
|
1
|
+
<!-- Resume anchor — template (sdlc-task skill, "pause").
|
|
2
2
|
For work BELOW the design-doc threshold: hotfix tier, bug investigations,
|
|
3
|
-
any interrupted
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
any interrupted work. Write it at every pause AND at stage transitions of
|
|
4
|
+
long-running work. Keep it where the work item can point at it: a file in
|
|
5
|
+
the repo or a comment on the issue, attached with `item_link`. Put its
|
|
6
|
+
next action in the item's next step with `item_edit`.
|
|
7
|
+
The bar: a fresh session given only this anchor proceeds without asking
|
|
8
|
+
the owner anything the anchor should have contained. No eval checks this
|
|
9
|
+
bar yet; the reviewer judges it. -->
|
|
8
10
|
|
|
9
11
|
## ⏸ Resume anchor — <date>
|
|
10
12
|
|
|
11
13
|
**Goal:** <one line, owner-verbatim where possible>
|
|
12
14
|
|
|
13
|
-
**State:** lifecycle `<active|paused>` · stage `<capture|spec|build|verify|
|
|
15
|
+
**State:** lifecycle `<active|paused>` · stage `<capture|spec|build|verify|learn>`
|
|
14
16
|
|
|
15
17
|
**Findings so far:** <POINTER to where they live — issue comment link, doc
|
|
16
18
|
path, graph entity id. Never duplicate content inline.>
|
package/work/lib/capability.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// scrubs by pattern. The rendered answer is scrubbed once more before it
|
|
16
16
|
// leaves the door (R12).
|
|
17
17
|
import { refuse, text, isoDate, stringList } from './verb-fields.mjs';
|
|
18
|
-
import { oneLine, scrubSecrets, ROW_LINE_MAX } from './questions.mjs';
|
|
18
|
+
import { oneLine, scrubSecrets, ROW_LINE_MAX, priorityOrderOf, byPriority, orderNote, waitsOnLine } from './questions.mjs';
|
|
19
19
|
import { listLine, legacySealOf, marksByItem, movementOf, pullsOfItem, readPullsByRepo, correctionsOf, daysBetween } from './delivery.mjs';
|
|
20
20
|
import { acceptCodeRead, codeReadBrief } from './code-read.mjs';
|
|
21
21
|
|
|
@@ -259,7 +259,7 @@ export function createCapabilityVerbs(core) {
|
|
|
259
259
|
const born = fields.created_at ?? item.created_at ?? null;
|
|
260
260
|
proposed.push({ ...base, age_days: born ? daysBetween(born, readAt) : null, proposed_at: born, driver: clean(fields.driver ?? '', 64) });
|
|
261
261
|
} else if (lifecycle !== 'done' && !deliveries.length) {
|
|
262
|
-
omitted.push({ item: item.id, title: clean(item.title, 120), reason: `the item is ${clean(lifecycle, 32)}; paused and archived items are backlog (D3 rule 1)` });
|
|
262
|
+
omitted.push({ item: item.id, title: clean(item.title, 120), reason: lifecycle === 'dropped' ? 'the owner dropped this item' : `the item is ${clean(lifecycle, 32)}; paused and archived items are backlog (D3 rule 1)` });
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
inProgress.sort((a, b) => (a.days_since_last_change ?? Number.MAX_SAFE_INTEGER) - (b.days_since_last_change ?? Number.MAX_SAFE_INTEGER) || a.item.localeCompare(b.item));
|
|
@@ -382,8 +382,10 @@ export function createCapabilityVerbs(core) {
|
|
|
382
382
|
// does not carry: an item with none reads "unregistered", as the six-part
|
|
383
383
|
// answer already renders it.
|
|
384
384
|
const items = only ? rows.filter((row) => row.fields?.product === only) : rows;
|
|
385
|
-
|
|
386
|
-
const
|
|
385
|
+
// A dropped item is counted once, under "Dropped by you", never as unsorted.
|
|
386
|
+
const unsorted = (row) => !registered(row) && row.fields?.lifecycle !== 'dropped';
|
|
387
|
+
const unregisteredTotal = rows.filter(unsorted).length;
|
|
388
|
+
const unregisteredShown = items.filter(unsorted).length;
|
|
387
389
|
const marks = await marksFor(new Set(items.map((row) => row.id)));
|
|
388
390
|
const readAt = now();
|
|
389
391
|
let redactions = 0;
|
|
@@ -415,7 +417,7 @@ export function createCapabilityVerbs(core) {
|
|
|
415
417
|
// own, and it must not pass through the one-line flattener. The count is
|
|
416
418
|
// read after the walk, so the walk's own finds are counted too (L1).
|
|
417
419
|
const scrubbed = ({ text, ...rest }) => { const rows = cleanDeep(rest); return { ...rows, redactions: redactions + rendered.redactions, text }; };
|
|
418
|
-
const built = [], building = [], pending = [], possible = [];
|
|
420
|
+
const built = [], building = [], pending = [], possible = [], dropped = [];
|
|
419
421
|
for (const item of items) {
|
|
420
422
|
const fields = item.fields ?? {};
|
|
421
423
|
const { rows: deliveries, marks: own } = deliveriesOf(item, marks);
|
|
@@ -431,24 +433,24 @@ export function createCapabilityVerbs(core) {
|
|
|
431
433
|
}
|
|
432
434
|
const doneAt = fields.delivered_at ?? item.updated_at ?? item.created_at ?? null;
|
|
433
435
|
if (fields.lifecycle === 'done' && !deliveries.length && !(since && String(doneAt ?? '') < since)) built.push({ ...base, mark_id: null, verdict: 'done, no delivery mark', delivered_at: doneAt, prs: [], source: 'no mark yet', note: 'the item record says done; the done verb writes the mark' });
|
|
434
|
-
|
|
435
|
-
|
|
436
|
+
const waits = fields.waits_on && typeof fields.waits_on === 'object' ? { waits_on: fields.waits_on } : {};
|
|
437
|
+
if (fields.lifecycle === 'active') building.push({ ...base, ...waits, stage: clean(fields.stage ?? '', 64), driver: clean(fields.driver ?? '', 64), next_step: clean(fields.next_step ?? '', 200), days_since_last_change: days, last_change_source: clean(last?.source ?? 'nothing recorded', 120), delivery_state: fields.delivery_state ? clean(fields.delivery_state, 64) : null });
|
|
438
|
+
else if (fields.lifecycle === 'paused') pending.push({ ...base, ...waits, reason: clean(fields.pause_reason ?? 'no reason on record', 300), days_since_last_change: days, parked_at: fields.parked_at ?? null });
|
|
436
439
|
else if (fields.lifecycle === 'proposed') {
|
|
437
440
|
const born = fields.created_at ?? item.created_at ?? null;
|
|
438
|
-
possible.push({ ...base, age_days: born ? daysBetween(born, readAt) : null, proposed_at: born, driver: clean(fields.driver ?? '', 64) });
|
|
439
|
-
}
|
|
441
|
+
possible.push({ ...base, ...waits, age_days: born ? daysBetween(born, readAt) : null, proposed_at: born, driver: clean(fields.driver ?? '', 64) });
|
|
442
|
+
} else if (fields.lifecycle === 'dropped') dropped.push({ ...base, dropped_at: fields.dropped_at ?? null });
|
|
440
443
|
}
|
|
441
|
-
built.sort((a, b) => String(b.delivered_at ?? '').localeCompare(String(a.delivered_at ?? '')));
|
|
442
|
-
building.sort((a, b) => (a.days_since_last_change ?? Number.MAX_SAFE_INTEGER) - (b.days_since_last_change ?? Number.MAX_SAFE_INTEGER) || a.item.localeCompare(b.item));
|
|
443
|
-
pending.sort((a, b) => (a.days_since_last_change ?? Number.MAX_SAFE_INTEGER) - (b.days_since_last_change ?? Number.MAX_SAFE_INTEGER) || a.item.localeCompare(b.item));
|
|
444
|
-
possible.sort((a, b) => (b.age_days ?? 0) - (a.age_days ?? 0));
|
|
445
|
-
|
|
446
444
|
// The decisions that set the order. The owner's words are what ordered
|
|
447
|
-
// this work, so the view ends with them, newest first.
|
|
445
|
+
// this work, so the view ends with them, newest first. Release 0.2.3:
|
|
446
|
+
// his newest priority decision also orders the rows above.
|
|
448
447
|
const products = only ? [only] : [...new Set(items.filter(registered).map((row) => row.fields.product))];
|
|
449
448
|
const order = [];
|
|
449
|
+
const orders = new Map();
|
|
450
450
|
for (const productId of products) {
|
|
451
|
-
|
|
451
|
+
const decisions = await decisionsFor(productId);
|
|
452
|
+
orders.set(productId, priorityOrderOf(decisions, productId));
|
|
453
|
+
for (const row of decisions) {
|
|
452
454
|
if (since && String(row.fields?.date ?? '') < since) continue;
|
|
453
455
|
if (row.fields?.kind === 'priority' || (Array.isArray(row.fields?.priority_order) && row.fields.priority_order.length)) {
|
|
454
456
|
order.push({ decision: row.id, product: productId, kind: row.fields?.kind ?? 'decision', date: row.fields?.date ?? '', text: clean(row.fields?.text ?? row.title, 400), priority_order: row.fields?.priority_order ?? [], citation: { decision: row.id, date: row.fields?.date ?? '', source: row.fields?.source ?? '' } });
|
|
@@ -456,6 +458,12 @@ export function createCapabilityVerbs(core) {
|
|
|
456
458
|
}
|
|
457
459
|
}
|
|
458
460
|
order.sort((a, b) => String(b.date).localeCompare(String(a.date)));
|
|
461
|
+
const byDays = (a, b) => (a.days_since_last_change ?? Number.MAX_SAFE_INTEGER) - (b.days_since_last_change ?? Number.MAX_SAFE_INTEGER) || a.item.localeCompare(b.item);
|
|
462
|
+
built.sort((a, b) => String(b.delivered_at ?? '').localeCompare(String(a.delivered_at ?? '')));
|
|
463
|
+
building.sort(byPriority(orders, byDays));
|
|
464
|
+
pending.sort(byPriority(orders, byDays));
|
|
465
|
+
possible.sort(byPriority(orders, (a, b) => (b.age_days ?? 0) - (a.age_days ?? 0)));
|
|
466
|
+
const waitsOf = (row) => (row.waits_on ? ` · ${waitsOnLine(row.waits_on)}` : '');
|
|
459
467
|
|
|
460
468
|
// D3 rule 2: a row names its product. The board does not group, so every
|
|
461
469
|
// line carries the id (review H6).
|
|
@@ -464,20 +472,23 @@ export function createCapabilityVerbs(core) {
|
|
|
464
472
|
const block = [
|
|
465
473
|
`Built, pending, possible — ${only ?? `every product${unregisteredShown ? `, and ${plural(unregisteredShown, 'item')} that name${unregisteredShown === 1 ? 's' : ''} none` : ''}`} · read ${readAt.slice(0, 19)}Z${since ? ` · since ${since}` : ''}`,
|
|
466
474
|
section(since ? `Built since ${since}` : 'Built', built, (row) => listLine(row.title, `${where_(row)} · ${row.verdict} ${day(row.delivered_at)} · ${prsOf(row)} · ${row.item}${row.mark_id ? ` · mark ${row.mark_id.slice(0, 8)}` : ''}`)),
|
|
467
|
-
section('Building', building, (row) => listLine(`${row.title} · ${row.stage || 'no stage'} · next: ${row.next_step || 'not set'}`, `${where_(row)} · ${row.item} · ${row.days_since_last_change === null ? 'no movement on record' : `${plural(row.days_since_last_change, 'day')} since ${row.last_change_source}`}`)),
|
|
468
|
-
section('Pending, parked until someone returns to it', pending, (row) => listLine(`${row.title} — ${row.reason}`, `${where_(row)} · ${row.item}`)),
|
|
469
|
-
section('Possible, proposed and waiting for a yes', possible, (row) => listLine(row.title
|
|
475
|
+
section('Building', building, (row) => listLine(`${row.title} · ${row.stage || 'no stage'}${waitsOf(row)} · next: ${row.next_step || 'not set'}`, `${where_(row)} · ${row.item} · ${row.days_since_last_change === null ? 'no movement on record' : `${plural(row.days_since_last_change, 'day')} since ${row.last_change_source}`}${orderNote(orders, row)}`)),
|
|
476
|
+
section('Pending, parked until someone returns to it', pending, (row) => listLine(`${row.title}${waitsOf(row)} — ${row.reason}`, `${where_(row)} · ${row.item}${orderNote(orders, row)}`)),
|
|
477
|
+
section('Possible, proposed and waiting for a yes', possible, (row) => listLine(`${row.title}${waitsOf(row)}`, `${where_(row)} · ${row.age_days === null ? 'age unknown' : `${plural(row.age_days, 'day')} old`} · ${row.item}${orderNote(orders, row)}`)),
|
|
470
478
|
section('The decisions that set the order', order, (row) => listLine(`${row.kind}: ${row.text}`, `${row.product} · ${row.date} · ${row.decision}`)),
|
|
471
|
-
...(
|
|
472
|
-
|
|
479
|
+
...(dropped.length ? [`Dropped by you (${dropped.length}): they stay in the graph, and no live list names them.`] : []),
|
|
480
|
+
// Release 0.2.3: a read narrowed to
|
|
481
|
+
// one product says nothing about items that name no product. The
|
|
482
|
+
// count stays in the result, as unregistered_total.
|
|
483
|
+
...(unregisteredTotal && !only ? [[
|
|
484
|
+
`Not in the registry (${unregisteredTotal}, ${unregisteredShown} of them in the rows above, named "unregistered")`,
|
|
473
485
|
' The slice L5 sweep gives them a product.',
|
|
474
|
-
...(only ? [' A read narrowed to one product cannot show them: an item with no product belongs to none.'] : []),
|
|
475
486
|
].join('\n')] : []),
|
|
476
487
|
].join('\n\n');
|
|
477
488
|
const rendered = scrubSecrets(block);
|
|
478
489
|
return scrubbed({
|
|
479
490
|
verb: 'built_pending_possible', read_at: readAt, product: only, since,
|
|
480
|
-
built, building, pending, possible, order, unregistered_total: unregisteredTotal, unregistered_in_rows: unregisteredShown,
|
|
491
|
+
built, building, pending, possible, order, dropped_total: dropped.length, unregistered_total: unregisteredTotal, unregistered_in_rows: unregisteredShown,
|
|
481
492
|
rows: built.length + building.length + pending.length + possible.length,
|
|
482
493
|
redactions: redactions + rendered.redactions, text: rendered.text,
|
|
483
494
|
});
|
package/work/lib/delivery.mjs
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
// reopen points at the delivery it corrects with the corrects edge (R9).
|
|
13
13
|
// 3. Movement is read, never stored. The view computes it at question time
|
|
14
14
|
// from the marks and from the repos' pull requests (D3 rule 5, R1).
|
|
15
|
-
import { refuse, text, isoDate, url, stringList, sha256 } from './verb-fields.mjs';
|
|
16
|
-
import { oneLine, ROW_LINE_MAX } from './questions.mjs';
|
|
15
|
+
import { refuse, text, isoDate, url, stringList, sha256, repoKinds, LIVE_KINDS } from './verb-fields.mjs';
|
|
16
|
+
import { oneLine, ROW_LINE_MAX, priorityOrderOf, byPriority, orderNote, waitsOnLine } from './questions.mjs';
|
|
17
17
|
|
|
18
18
|
export const DELIVERY_WRITE_VERBS = Object.freeze(['item_done', 'item_reopen']);
|
|
19
19
|
export const DELIVERY_READ_VERBS = Object.freeze(['in_flight']);
|
|
@@ -353,14 +353,20 @@ export function createDeliveryVerbs(core) {
|
|
|
353
353
|
// or a repo whose kind was never set, keeps the three checks. Nothing
|
|
354
354
|
// weakens by omission, and a mixed-kind item takes the strict path
|
|
355
355
|
// because one of its repos demands it.
|
|
356
|
+
//
|
|
357
|
+
// Stage 1 reads a list of kinds. The strict path applies when any kind
|
|
358
|
+
// of any named repo is live (service or schema). A cli and a plugin
|
|
359
|
+
// prove delivery like a library until the trial install of stage 2.
|
|
356
360
|
const named = (Array.isArray(fields.repos) ? fields.repos : []).map((id) => reg.repos.get(id)).filter(Boolean);
|
|
357
|
-
const
|
|
358
|
-
const
|
|
361
|
+
const kindsOf = (repo) => repoKinds(repo.fields);
|
|
362
|
+
const said = (repo) => `${repo.fields.registry_id} is a ${kindsOf(repo).join(' and ')}`;
|
|
363
|
+
const unset = named.filter((repo) => !kindsOf(repo).length).map((repo) => repo.fields.registry_id);
|
|
364
|
+
const strictKinds = named.filter((repo) => kindsOf(repo).some((kind) => LIVE_KINDS.includes(kind))).map(said);
|
|
359
365
|
const live = !named.length || unset.length > 0 || strictKinds.length > 0;
|
|
360
366
|
const healthUrl = text(product.fields?.health_url, `product ${product.fields.registry_id} health_url`, { optional: true, max: 2000 });
|
|
361
367
|
if (live && !healthUrl) {
|
|
362
368
|
const why = !named.length ? 'the item names no registered repo' : (unset.length ? `repo ${unset.join(', ')} carries no kind` : `${strictKinds.join(', ')}`);
|
|
363
|
-
refuse(`product ${product.fields.registry_id} has no health_url, and the live checks apply because ${why}; the owner sets it with product edit, or sets the repo
|
|
369
|
+
refuse(`product ${product.fields.registry_id} has no health_url, and the live checks apply because ${why}; the owner sets it with product edit, or sets the repo kinds with repo edit; a library, a cli and a plugin are proved by the merge`);
|
|
364
370
|
}
|
|
365
371
|
// Which pull requests deliver this item. The caller names them, always.
|
|
366
372
|
// An earlier draft fell back to the item's own pr links, and the first
|
|
@@ -373,7 +379,7 @@ export function createDeliveryVerbs(core) {
|
|
|
373
379
|
if (!given.length) refuse('prs must name at least one merged pull request');
|
|
374
380
|
let spec = null;
|
|
375
381
|
if (live) { spec = functionalSpec(args.functional); allowedPath(spec, product, healthUrl); }
|
|
376
|
-
else if (args.functional !== undefined) refuse(`functional is not accepted for this delivery: ${named.map(
|
|
382
|
+
else if (args.functional !== undefined) refuse(`functional is not accepted for this delivery: ${named.map(said).join(', ')}, so there is no deployed page to read. The merge and the gate prove it.`);
|
|
377
383
|
const note = text(args.note, 'note', { optional: true, max: 2000 }) ?? '';
|
|
378
384
|
|
|
379
385
|
const repos = new Set(Array.isArray(fields.repos) ? fields.repos : []);
|
|
@@ -434,7 +440,7 @@ export function createDeliveryVerbs(core) {
|
|
|
434
440
|
const checks = live ? [contains, health, functional] : [{
|
|
435
441
|
name: 'every named pull request is merged',
|
|
436
442
|
result: 'pass',
|
|
437
|
-
detail: `${pulls.length} pull request${pulls.length === 1 ? '' : 's'} merged on GitHub: ${pulls.map((pull) => `${pull.url} at ${pull.merge_commit_sha.slice(0, 7)}`).join(', ')}. No live check ran: ${named.map(
|
|
443
|
+
detail: `${pulls.length} pull request${pulls.length === 1 ? '' : 's'} merged on GitHub: ${pulls.map((pull) => `${pull.url} at ${pull.merge_commit_sha.slice(0, 7)}`).join(', ')}. No live check ran: ${named.map(said).join(', ')}, so the product has no deployed endpoint to read (amendment A1.7).`,
|
|
438
444
|
checked_at: now(),
|
|
439
445
|
}];
|
|
440
446
|
const mark = await create(type, requestId, 'item_done', {
|
|
@@ -545,6 +551,11 @@ export function createDeliveryVerbs(core) {
|
|
|
545
551
|
if (repoIds.length > MAX_REPOS) refuse(`${repoIds.length} repos are registered for this view; it reads at most ${MAX_REPOS}. Narrow it with a product id.`);
|
|
546
552
|
const { pullsByRepo, repoErrors } = await readPullsByRepo(repoIds, readGitHub);
|
|
547
553
|
|
|
554
|
+
// Release 0.2.3: the owner's priority decision orders the rows.
|
|
555
|
+
const decisionType = await typeNamed('decision');
|
|
556
|
+
const decisions = await client.listAllEntities({ type: decisionType.name, state: 'active' });
|
|
557
|
+
const orders = new Map([...new Set(items.map((row) => row.fields.product))].map((productId) => [productId, priorityOrderOf(decisions, productId)]));
|
|
558
|
+
|
|
548
559
|
const readAt = now();
|
|
549
560
|
const rows = [], delivered = [], notMarked = [];
|
|
550
561
|
for (const item of items) {
|
|
@@ -583,21 +594,23 @@ export function createDeliveryVerbs(core) {
|
|
|
583
594
|
// The id is what a reader types back, so the title gives way to it
|
|
584
595
|
// inside the line budget, as the "Needs me" row does (review G6).
|
|
585
596
|
oneLine(`${oneLine(item.title, Math.max(1, ROW_LINE_MAX - item.id.length - 3))} · ${item.id}`),
|
|
586
|
-
oneLine(`${fields.product} · ${fields.stage ?? 'no stage'} · ${fields.driver ?? 'no driver'} · next: ${fields.next_step || 'not set'}`),
|
|
587
|
-
oneLine(`${days === null ? 'no movement on record' : `${days} day${days === 1 ? '' : 's'} since last change`} (${last?.source ?? 'nothing recorded'}) · ${mine.length} pull request${mine.length === 1 ? '' : 's'}${open.length ? ` · ${open.length} merged, not marked` : ''}`),
|
|
597
|
+
oneLine(`${fields.product} · ${fields.stage ?? 'no stage'} · ${fields.driver ?? 'no driver'}${waitsOnLine(fields.waits_on) ? ` · ${waitsOnLine(fields.waits_on)}` : ''} · next: ${fields.next_step || 'not set'}`),
|
|
598
|
+
oneLine(`${days === null ? 'no movement on record' : `${days} day${days === 1 ? '' : 's'} since last change`} (${last?.source ?? 'nothing recorded'}) · ${mine.length} pull request${mine.length === 1 ? '' : 's'}${open.length ? ` · ${open.length} merged, not marked` : ''}${orderNote(orders, { product: fields.product, item: item.id })}`),
|
|
588
599
|
],
|
|
600
|
+
waits_on: fields.waits_on ?? null,
|
|
589
601
|
folded: { title: item.title, next_step: fields.next_step ?? '', scope: fields.scope?.text ?? '', pieces: fields.split_children ?? [], prs: mine.map((pull) => ({ url: pull.url, state: pull.state, matched_by: pull.matched_by, merged_at: pull.merged_at, updated_at: pull.updated_at })), marks: own.map((mark) => ({ id: mark.id, kind: mark.fields.kind, verdict: mark.fields.verdict, at: mark.fields.marked_at })) },
|
|
590
602
|
citation: { item: item.id, read_at: readAt, repos: [...new Set(mine.map((pull) => pull.repo))] },
|
|
591
603
|
});
|
|
592
604
|
}
|
|
593
605
|
// The owner's rule: order by days since last change, latest on top, and
|
|
594
|
-
// show the days on the row.
|
|
595
|
-
|
|
606
|
+
// show the days on the row. Release 0.2.3: the items his priority
|
|
607
|
+
// decision names come first, in his order.
|
|
608
|
+
rows.sort(byPriority(orders, (a, b) => (a.days_since_last_change ?? Number.MAX_SAFE_INTEGER) - (b.days_since_last_change ?? Number.MAX_SAFE_INTEGER) || a.item.localeCompare(b.item)));
|
|
596
609
|
delivered.sort((a, b) => String(b.delivered_at ?? '').localeCompare(String(a.delivered_at ?? '')));
|
|
597
610
|
notMarked.sort((a, b) => (a.days ?? 0) - (b.days ?? 0));
|
|
598
611
|
const products = [...new Set(rows.map((row) => row.product))].sort();
|
|
599
612
|
const byProduct = products.map((product) => ({ product, rows: rows.filter((row) => row.product === product) }));
|
|
600
|
-
const block = byProduct.map((group) => [`${group.product} — in flight (${group.rows.length})`, ...group.rows.flatMap((row, index) => row.lines.map((line, position) => `${position === 0 ? `${index + 1}. ` : ' '}${line}`))].join('\n'));
|
|
613
|
+
const block = byProduct.map((group) => [`${group.product} — in flight (${group.rows.length})${orders.get(group.product) ? ` · in your order of ${orders.get(group.product).date}, then by the last change` : ''}`, ...group.rows.flatMap((row, index) => row.lines.map((line, position) => `${position === 0 ? `${index + 1}. ` : ' '}${line}`))].join('\n'));
|
|
601
614
|
if (notMarked.length) block.push([`merged, not marked (${notMarked.length})`, ...notMarked.map((row) => listLine(row.title, `${row.pr} · ${row.item} · merged ${row.days} day${row.days === 1 ? '' : 's'} ago`))].join('\n'));
|
|
602
615
|
if (repoErrors.length) block.push([`GitHub did not answer for ${repoErrors.length} repo${repoErrors.length === 1 ? '' : 's'}; movement below is from the graph only`, ...repoErrors.map((row) => oneLine(` ${row.repo} — ${row.reason}`, ROW_LINE_MAX))].join('\n'));
|
|
603
616
|
if (unregisteredTotal && !only) {
|
|
@@ -615,6 +628,7 @@ export function createDeliveryVerbs(core) {
|
|
|
615
628
|
if (since && delivered.length) block.push([`delivered since ${since} (${delivered.length})`, ...delivered.map((row) => listLine(row.item_title, `${row.verdict}${(row.gate_warnings ?? []).length ? `, gate warning on ${row.gate_warnings.map((warning) => warning.check).join(' ')}` : ''} · ${String(row.delivered_at ?? '').slice(0, 10)} · ${row.prs.join(' ') || 'no pull request on the mark'}`))].join('\n'));
|
|
616
629
|
return {
|
|
617
630
|
verb: 'in_flight', read_at: readAt, product: only, since, rows: rows.length, products: byProduct,
|
|
631
|
+
priority: Object.fromEntries([...orders].filter(([, order]) => order).map(([productId, order]) => [productId, { decision: order.decision, date: order.date }])),
|
|
618
632
|
merged_not_marked: notMarked, delivered: since ? delivered : delivered.slice(0, 20), unregistered, unregistered_total: unregisteredTotal, repos_read: repoIds, repo_errors: repoErrors,
|
|
619
633
|
text: block.join('\n\n') || 'Nothing is in flight: no active item on a registered product.',
|
|
620
634
|
};
|