create-pmos 0.132.0 → 0.134.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.
Files changed (64) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/bin/pmos.js +18 -2
  7. package/workspace/guide/index.md +1 -0
  8. package/workspace/guide/notes.md +19 -1
  9. package/workspace/guide/welcome.md +38 -0
  10. package/workspace/lib/import-overlay.js +32 -0
  11. package/workspace/lib/notes-audio.js +28 -9
  12. package/workspace/lib/notes-capture.js +128 -0
  13. package/workspace/lib/notes-live-guard.js +117 -0
  14. package/workspace/lib/notes-whisper.js +132 -16
  15. package/workspace/lib/notes.js +4 -2
  16. package/workspace/lib/route-modules.js +1 -0
  17. package/workspace/lib/routes-notes.js +60 -8
  18. package/workspace/lib/routes-onboarding.js +215 -0
  19. package/workspace/lib/vendor/scripts/anchor-resolve.js +351 -0
  20. package/workspace/lib/vendor/scripts/delivery-index.js +356 -0
  21. package/workspace/lib/vendor/scripts/intake.js +78 -0
  22. package/workspace/lib/vendor/scripts/lane-defs.js +226 -0
  23. package/workspace/lib/vendor/scripts/migrate-layout.js +775 -0
  24. package/workspace/lib/vendor/scripts/nine-stages.js +81 -0
  25. package/workspace/lib/vendor/scripts/onboarding/apply.js +110 -0
  26. package/workspace/lib/vendor/scripts/onboarding/brief.js +154 -0
  27. package/workspace/lib/vendor/scripts/onboarding/first-briefing.js +41 -0
  28. package/workspace/lib/vendor/scripts/onboarding/first-look.js +84 -0
  29. package/workspace/lib/vendor/scripts/onboarding/infer-brief.js +70 -0
  30. package/workspace/lib/vendor/scripts/onboarding/proposals.js +98 -0
  31. package/workspace/lib/vendor/scripts/onboarding/scan-worker.js +27 -0
  32. package/workspace/lib/vendor/scripts/onboarding/scan.js +859 -0
  33. package/workspace/lib/vendor/scripts/onboarding/structure.js +217 -0
  34. package/workspace/lib/vendor/scripts/plan-items-stage.js +271 -0
  35. package/workspace/lib/vendor/scripts/read-jsonl.js +53 -0
  36. package/workspace/lib/vendor/scripts/signals/layout-paths.js +43 -0
  37. package/workspace/lib/vendor/scripts/status-facts.js +214 -0
  38. package/workspace/lib/vendor/scripts/trust/import-guard.js +36 -0
  39. package/workspace/lib/vendor/scripts/trust/ledger.js +53 -0
  40. package/workspace/lib/vendor/scripts/workflow-state.js +1888 -0
  41. package/workspace/lib/vendor/workspace/lib/actor.js +91 -0
  42. package/workspace/lib/vendor/workspace/lib/board-cache.js +156 -0
  43. package/workspace/lib/vendor/workspace/lib/console-state.js +295 -0
  44. package/workspace/lib/vendor/workspace/lib/doc-category.js +227 -0
  45. package/workspace/lib/vendor/workspace/lib/factory-join.js +158 -0
  46. package/workspace/lib/vendor/workspace/lib/git-info.js +306 -0
  47. package/workspace/lib/vendor/workspace/lib/plan-items.js +922 -0
  48. package/workspace/lib/vendor/workspace/lib/plan-tree.js +419 -0
  49. package/workspace/lib/vendor/workspace/lib/review-gate.js +39 -0
  50. package/workspace/lib/vendor/workspace/lib/sink.js +321 -0
  51. package/workspace/lib/vendor/workspace/lib/state-root.js +587 -0
  52. package/workspace/package-lock.json +2 -2
  53. package/workspace/package.json +1 -1
  54. package/workspace/public/pm/calendar-views.js +25 -12
  55. package/workspace/public/pm/components.css +280 -0
  56. package/workspace/public/pm/console3.css +19 -0
  57. package/workspace/public/pm/shell-recording.js +122 -12
  58. package/workspace/public/pm/view-notes.js +7 -2
  59. package/workspace/public/pm/welcome.js +283 -0
  60. package/workspace/public/welcome.html +20 -0
  61. package/workspace/server.js +2 -2
  62. package/workspace/test-files.json +2 -0
  63. package/workspace/test-lists/meeting-notes.json +5 -3
  64. package/workspace/test-lists/onboarding-firstrun.json +15 -0
@@ -0,0 +1,356 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /*
4
+ * PMOS delivery evidence — the one place git's history answers "did THIS initiative ship?".
5
+ *
6
+ * const D = require('./delivery-index');
7
+ * const idx = D.deliveryIndex(root, ids); // { ok, reason?, byBranch:Set, byMention:Set }
8
+ * D.attribution(idx, id) // 'branch' | 'mention' | 'change' | null | 'unknown'
9
+ * D.isDeliveryPath('workspace/server.js') // true — a build path, not planning prose
10
+ *
11
+ * WHY THIS EXISTS. This derivation shipped inside `workspace/server.js` as a module-private
12
+ * function, so it answered for exactly one surface. The engine — `scripts/workflow-state.js`, which
13
+ * CI, the CLI and the workspace ladder all run — could not see it, and derived its `build` rung from
14
+ * a signal carrying no attribution at all: `buildCommits` counted every commit in a range that
15
+ * touched a build path, with no idea which initiative it was answering about. Over a pull-request
16
+ * range that is correct, because the range IS one initiative's work. Over the trunk it reported the
17
+ * same 488 commits as evidence for all 177 initiatives, including ones that shipped nothing.
18
+ *
19
+ * So the GUI's initiative board and the engine's ladder held two different opinions about the same
20
+ * question, in the same process, three hundred lines apart — one attributed, one not. That is the
21
+ * shape `scripts/anchor-resolve.js` was extracted to end for `parent_kr`, stated in its own header,
22
+ * and delivery is the same kind of fact. See
23
+ * planning/explainers/workflow-status-sweep-surface-explainer.md.
24
+ *
25
+ * TWO SIGNALS, because one is not enough (board-delivery-truth R-2):
26
+ *
27
+ * 1. THE BRANCH CONVENTION is precise and incomplete. A merge whose subject names
28
+ * `initiative/<id>` is unambiguous — but 31% of this repository's merged pull requests shipped
29
+ * on `claude/*` branches, which match no id, and a convention nothing enforces cannot be the
30
+ * only signal.
31
+ *
32
+ * 2. A COMMIT THAT MENTIONS THE ID AND TOUCHES A BUILD PATH. The build-path requirement is what
33
+ * keeps the mention signal honest: `feat(planning): add <id> PRD` mentions the id while
34
+ * delivering nothing, and without the path test every SPECIFIED initiative would read as
35
+ * shipped — which is the same class of false green this module exists to remove.
36
+ *
37
+ * HONESTY BY CONSTRUCTION. `ok:false` is a real answer and never degrades into "nothing was
38
+ * delivered". A caller that cannot read the history must say so; silently reporting `planning` for
39
+ * every shipped initiative is precisely the failure being fixed. `attribution()` returns the string
40
+ * `'unknown'` for that case, and it is deliberately not `null` — "I cannot tell" and "I looked and
41
+ * there is nothing" call for opposite responses from every caller.
42
+ *
43
+ * Zero dependencies, stock Node. It runs in CI, in a terminal, inside the workspace server, and
44
+ * inside an adopter's vendored kit.
45
+ */
46
+
47
+ const { spawnSync } = require('child_process');
48
+
49
+ /*
50
+ * A path counts as DELIVERY when it is neither a planning artifact nor governance prose.
51
+ *
52
+ * This is deliberately a SEPARATE rule from the engine's `isBuildPath`, and the difference is not an
53
+ * oversight. `isBuildPath` answers "is this commit implementation work for the lane under review?"
54
+ * and therefore lane-gates `prototype/` — in the prototype lane the specimen IS the deliverable
55
+ * (D60), in every other lane it is somebody else's leg-A work. This answers a coarser question
56
+ * asked of the whole trunk at once, where no single lane is under review, so `prototype/` is always
57
+ * excluded: a prototype never ships (D01), so a commit touching only `prototype/` is never evidence
58
+ * that something was delivered. Merging the two rules would force one of the two questions to be
59
+ * answered wrongly. They are kept apart, and each says why in the place it is written.
60
+ */
61
+ const EXCLUDED_PREFIX = /^(planning|okf|prototype)\//;
62
+ function isDeliveryPath(f) {
63
+ if (!f) return false;
64
+ if (EXCLUDED_PREFIX.test(f)) return false;
65
+ // Root-level governance prose (DECISIONS.md, CLAUDE.md, AGENTS.md…) is not a build. A markdown
66
+ // file deeper in the tree may well be — `workspace/docs/*.md` ships with the app — so the test is
67
+ // on the shape of the path, not on the extension alone.
68
+ return !/^[^/]+\.md$/.test(f);
69
+ }
70
+
71
+ function gitCap(root, args) {
72
+ try {
73
+ const r = spawnSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
74
+ if (r.error || r.status !== 0) {
75
+ return { ok: false, out: '', err: (r.stderr || (r.error && r.error.message) || '').trim() };
76
+ }
77
+ return { ok: true, out: r.stdout || '' };
78
+ } catch (e) { return { ok: false, out: '', err: String((e && e.message) || e) }; }
79
+ }
80
+
81
+ function trunkRef(root) {
82
+ for (const c of ['origin/main', 'main', 'origin/master', 'master']) {
83
+ const r = gitCap(root, ['rev-parse', '--verify', '--quiet', c]);
84
+ if (r.ok && r.out.trim()) return { ref: c, head: r.out.trim() };
85
+ }
86
+ return null;
87
+ }
88
+
89
+ // ONE `git log` per trunk head, memoised on that sha — ~0.3 s and ~800 KB on this repository's
90
+ // 1,200 commits, paid once and reused by every caller until the trunk moves. The cache key is the
91
+ // head sha rather than the root, so a fetch that advances the trunk invalidates it by construction
92
+ // rather than by anyone remembering to.
93
+ const deliveryCache = new Map(); // root -> { head, index }
94
+
95
+ /**
96
+ * Which initiatives the trunk's own history says were delivered.
97
+ *
98
+ * @param {string} root repository root
99
+ * @param {Iterable<string>} ids the known initiative ids — the mention signal can only recognise
100
+ * ids it was told about, and matching arbitrary tokens would turn
101
+ * any commit subject into a delivery claim
102
+ * @returns {{ok:boolean, reason?:string, byBranch:Set<string>, byMention:Set<string>}}
103
+ */
104
+ function deliveryIndex(root, ids, opts) {
105
+ const t = trunkRef(root);
106
+ if (!t) {
107
+ return {
108
+ ok: false,
109
+ reason: 'no trunk branch (main/master) is reachable from this repository — delivery cannot be derived',
110
+ byBranch: new Set(), byMention: new Set(), byChange: new Set(),
111
+ };
112
+ }
113
+ const hit = deliveryCache.get(root);
114
+ if (hit && hit.head === t.head) return hit.index;
115
+
116
+ // %x1e separates commits, %x1f separates the message from --name-only's file list.
117
+ const r = gitCap(root, ['log', '--format=%x1e%s %b%x1f', '--name-only', t.head]);
118
+ if (!r.ok) {
119
+ return {
120
+ ok: false,
121
+ reason: `git could not read the history at ${t.ref}${r.err ? ` — ${r.err}` : ''}`,
122
+ byBranch: new Set(), byMention: new Set(), byChange: new Set(),
123
+ };
124
+ }
125
+ const byBranch = new Set();
126
+ const byMention = new Set();
127
+ const known = Array.isArray(ids) ? ids : [...(ids || [])];
128
+ for (const chunk of r.out.split('\x1e')) {
129
+ if (!chunk.trim()) continue;
130
+ const cut = chunk.indexOf('\x1f');
131
+ const msg = cut < 0 ? chunk : chunk.slice(0, cut);
132
+ const files = cut < 0 ? [] : chunk.slice(cut + 1).split('\n').map((s) => s.trim()).filter(Boolean);
133
+ for (const m of msg.matchAll(/initiative\/([A-Za-z0-9][\w.-]*)/g)) byBranch.add(m[1]);
134
+ if (!files.some(isDeliveryPath)) continue;
135
+ // `id.length > 3` guards against a short id matching incidentally inside an unrelated word.
136
+ // It is a heuristic and is named as one in the PRD's risks; a miss reads as `pending`, which is
137
+ // the safe direction and the one the surface already errs in.
138
+ for (const id of known) if (id.length > 3 && msg.includes(id)) byMention.add(id);
139
+ }
140
+ // 3. THE CHANGE THAT CARRIED THE SPEC (scan-consistency R1). See changeSignal below.
141
+ const byChange = changeSignal(root, t.head, known, opts);
142
+ const index = { ok: true, byBranch, byMention, byChange };
143
+ deliveryCache.set(root, { head: t.head, index });
144
+ return index;
145
+ }
146
+
147
+ /*
148
+ * THE THIRD SIGNAL — a change on the trunk that touched this initiative's OWN documents and build
149
+ * paths together (scan-consistency R1).
150
+ *
151
+ * Signals 1 and 2 both need the id spelled in a commit message. On this repository most pull
152
+ * requests ship on `claude/<slug>-<suffix>` branches and their commits name the area (`feat(workspace):
153
+ * …`), not the id, so 102 of 283 initiatives read `build pending` although their PRD, record and code
154
+ * merged in one pull request. The tie is concrete and git holds it: the pull request that changed
155
+ * `planning/initiatives/<id>.md` (or the PRD the record names) ALSO changed build files.
156
+ *
157
+ * A UNIT is one first-parent commit of the trunk: a merge commit (its diff against the first parent
158
+ * is the whole pull request), a squash merge, or a direct commit. Two guards keep it from becoming a
159
+ * similarity score:
160
+ * · the unit must change at least one build path (isDeliveryPath) — a docs-only pull request
161
+ * delivers nothing;
162
+ * · the unit may touch the documents of at most MAX_CO_INITIATIVES initiatives. A sweep that edits
163
+ * forty records while changing a script is evidence for none of them.
164
+ * A merged branch whose name, less its random suffix, IS a known id (`claude/jira-wbs-kpamaz`) counts
165
+ * too, when its pull request changed build paths.
166
+ */
167
+ const MAX_CO_INITIATIVES = 2;
168
+ const RECORD_DIRS = ['planning/initiatives', 'pmos/state/initiatives', 'state/initiatives'];
169
+ /** { [repo-relative path]: Set<id> } — each record and every `*_path` its frontmatter names. */
170
+ function documentOwners(root, ids, recordsDir) {
171
+ const fs = require('fs');
172
+ const path = require('path');
173
+ const dirs = recordsDir ? [recordsDir] : RECORD_DIRS.map((d) => path.join(root, d));
174
+ const owners = new Map();
175
+ const add = (rel, id) => {
176
+ const k = String(rel || '').trim().replace(/^["']|["']$/g, '').replace(/^\.?\//, '');
177
+ if (!k || /\s/.test(k) || k.includes('..')) return;
178
+ if (!owners.has(k)) owners.set(k, new Set());
179
+ owners.get(k).add(id);
180
+ };
181
+ for (const id of ids) {
182
+ for (const dir of dirs) {
183
+ const file = path.join(dir, id + '.md');
184
+ let text;
185
+ try { text = fs.readFileSync(file, 'utf8'); } catch { continue; }
186
+ add(path.relative(root, file).split(path.sep).join('/'), id);
187
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
188
+ if (fm) for (const m of fm[1].matchAll(/^[a-z_]*_path[ \t]*:[ \t]*(\S.*?)[ \t]*$/gm)) add(m[1], id);
189
+ break;
190
+ }
191
+ }
192
+ return owners;
193
+ }
194
+ function changeSignal(root, head, known, opts) {
195
+ const out = new Set();
196
+ if (!known.length) return out;
197
+ const owners = documentOwners(root, known, opts && opts.recordsDir);
198
+ const ids = new Set(known);
199
+ const r = gitCap(root, ['log', '--first-parent', '--diff-merges=first-parent', '--format=%x1e%s%x1f', '--name-only', head]);
200
+ if (!r.ok) return out;
201
+ for (const chunk of r.out.split('\x1e')) {
202
+ if (!chunk.trim()) continue;
203
+ const cut = chunk.indexOf('\x1f');
204
+ const subject = cut < 0 ? chunk : chunk.slice(0, cut);
205
+ const files = cut < 0 ? [] : chunk.slice(cut + 1).split('\n').map((s) => s.trim()).filter(Boolean);
206
+ if (!files.some(isDeliveryPath)) continue;
207
+ const touched = new Set();
208
+ for (const f of files) for (const id of owners.get(f) || []) touched.add(id);
209
+ if (touched.size && touched.size <= MAX_CO_INITIATIVES) for (const id of touched) out.add(id);
210
+ const b = /^Merge (?:pull request #\d+ from|branch ')\s*(?:[^/\s]+\/)?(?:claude|feature|feat|fix|chore)\/([A-Za-z0-9][\w.-]*?)'?(?:\s|$)/.exec(subject.trim());
211
+ if (b) {
212
+ const slug = b[1];
213
+ const bare = slug.replace(/-[a-z0-9]{4,8}$/, '');
214
+ if (ids.has(slug)) out.add(slug);
215
+ else if (bare !== slug && ids.has(bare)) out.add(bare);
216
+ }
217
+ }
218
+ return out;
219
+ }
220
+
221
+ /*
222
+ * The branches that actually MERGED — a strictly narrower signal than `byBranch`, and the two are
223
+ * kept apart on purpose.
224
+ *
225
+ * `deliveryIndex` scans every commit's subject AND body, because a squash-merged pull request often
226
+ * names its branch only in the body. That breadth is right for ATTRIBUTION, where the question is
227
+ * "is there evidence this initiative's work landed" and a stray match against an id that already has
228
+ * a record costs nothing.
229
+ *
230
+ * It is wrong for any caller that RE-CREATES something from the match. A commit body quoting
231
+ * `initiative/foo` inside a code example is a real occurrence in this repository's history, and a
232
+ * caller that wrote a record for every token it found would manufacture initiatives named `foo`,
233
+ * `save_okr` and `pmos-operating-guide.` out of prose. So this reads MERGE SUBJECTS only, and
234
+ * requires the id to have the shape ids actually have. On this repository that is the difference
235
+ * between 4 fabricated records and the 1 that is genuinely missing.
236
+ */
237
+ const ID_SHAPE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
238
+ function mergedBranches(root) {
239
+ const t = trunkRef(root);
240
+ if (!t) return { ok: false, reason: 'no trunk branch (main/master) is reachable from this repository', ids: new Map() };
241
+ const r = gitCap(root, ['log', '--merges', '--format=%x1e%H %ct%x1f%s', t.head]);
242
+ if (!r.ok) return { ok: false, reason: `git could not read the merge history at ${t.ref}`, ids: new Map() };
243
+ const ids = new Map(); // id -> { sha, at (unix s), subject }
244
+ for (const chunk of r.out.split('\x1e')) {
245
+ if (!chunk.trim()) continue;
246
+ const [head, subject] = chunk.split('\x1f');
247
+ const [sha, ct] = String(head || '').trim().split(/\s+/);
248
+ const subj = String(subject || '').replace(/\s+/g, ' ').trim();
249
+ /*
250
+ * A BACK-merge — `merge: main into initiative/x` — merges the trunk INTO the branch, and it is
251
+ * still EVIDENCE THE BRANCH LANDED: it is only reachable from the trunk at all because the
252
+ * branch was subsequently merged in. What it must not do is supply the record's title and date,
253
+ * because it describes mid-flight housekeeping rather than the work, and its date is the
254
+ * housekeeping's. So it counts, and a landing merge outranks it whenever one exists. The
255
+ * direction is stated in the subject, so this is a fact rather than a guess.
256
+ */
257
+ const back = /\binto\s+\S*initiative\//i.test(subj);
258
+ for (const m of subj.matchAll(/initiative\/([A-Za-z0-9][\w.-]*)/g)) {
259
+ const id = m[1].replace(/[.,;`'")\]]+$/, '');
260
+ if (!ID_SHAPE.test(id)) continue;
261
+ const held = ids.get(id);
262
+ // The log runs newest-first, so the first LANDING merge seen is the most recent one — and it
263
+ // always wins over a back-merge, whenever it is found.
264
+ if (held && (!held.back || back)) continue;
265
+ ids.set(id, { sha: sha || null, at: Number.isFinite(parseInt(ct, 10)) ? parseInt(ct, 10) : null, subject: subj, back });
266
+ }
267
+ }
268
+ return { ok: true, ids };
269
+ }
270
+
271
+ /*
272
+ * `delivered_by:` — the PM's lever, and the check that keeps it from becoming a status field (R-2b).
273
+ *
274
+ * Both signals above are CONVENTIONS. Work delivered under a neighbouring initiative's branch and
275
+ * commit names satisfies neither, and on this repository that is not hypothetical: the initiative the
276
+ * PM opened has a PRD, a rubric and shipped code, and no commit on the trunk names it. `null` is the
277
+ * honest reading of the evidence and it leaves the PM holding a true statement with no way to correct
278
+ * it.
279
+ *
280
+ * So a record may name the commit its work landed in. The reason this does not reintroduce the
281
+ * declared status that D64 and the engine's own header forbid: `delivered_by` declares a **fact git
282
+ * can check**, in the shape `reconstructed_from:` already established for a merge date — not a
283
+ * verdict about where the work stands. The check is the whole point, and it is deliberately strict in
284
+ * BOTH directions:
285
+ *
286
+ * · reachable from the trunk AND touching at least one build path → `{ok:true}`; the caller
287
+ * attributes with signal `declared` and shows the sha it verified.
288
+ * · unreachable, unparseable, or reachable while touching nothing that counts as a build →
289
+ * `{ok:false, reason}`. The caller renders that BLOCKED, never `done`. A declaration that fails
290
+ * its own check is worse than no declaration, because a reader takes it for evidence.
291
+ */
292
+ function declaredDelivery(root, declared) {
293
+ const raw = String(declared == null ? '' : declared).trim();
294
+ if (!raw) return { declared: false };
295
+ // Take the leading token: the field is `<commit-ish> — free prose`, in the same shape as the
296
+ // record's other declared keys, so the prose stays available to the PM without breaking the check.
297
+ const token = raw.split(/[\s,]/)[0].replace(/[.,;]+$/, '');
298
+ if (!token) return { declared: true, ok: false, reason: 'delivered_by is present but names nothing' };
299
+ const t = trunkRef(root);
300
+ if (!t) {
301
+ return { declared: true, ok: false, unknown: true, reason: 'no trunk branch (main/master) is reachable — delivered_by cannot be verified here' };
302
+ }
303
+ const rev = gitCap(root, ['rev-parse', '--verify', '--quiet', `${token}^{commit}`]);
304
+ if (!rev.ok || !rev.out.trim()) {
305
+ return { declared: true, ok: false, token, reason: `delivered_by names '${token}', which is not a commit in this repository` };
306
+ }
307
+ const sha = rev.out.trim();
308
+ const anc = gitCap(root, ['merge-base', '--is-ancestor', sha, t.head]);
309
+ if (!anc.ok) {
310
+ return { declared: true, ok: false, token, sha, reason: `delivered_by names ${sha.slice(0, 8)}, which is not reachable from ${t.ref} — it has not landed on the trunk` };
311
+ }
312
+ const files = gitCap(root, ['show', '--format=', '--name-only', sha]);
313
+ const touched = files.ok ? files.out.split('\n').map((s) => s.trim()).filter(Boolean) : [];
314
+ if (!touched.some(isDeliveryPath)) {
315
+ return {
316
+ declared: true, ok: false, token, sha,
317
+ reason: `delivered_by names ${sha.slice(0, 8)}, which changes no build path — naming a planning commit as the delivery is the false green this check exists to catch`,
318
+ };
319
+ }
320
+ return { declared: true, ok: true, token, sha };
321
+ }
322
+
323
+ /**
324
+ * Which signal — if any — attributes a delivery to this initiative.
325
+ *
326
+ * `'unknown'` is returned when the index could not be built, and is deliberately NOT `null`. A
327
+ * caller that collapses the two reports "nothing was delivered" for a repository whose history it
328
+ * failed to read, which is a claim the missing evidence cannot support.
329
+ *
330
+ * @returns {'branch'|'mention'|'change'|'unknown'|null}
331
+ */
332
+ function attribution(index, id) {
333
+ if (!index || !index.ok) return 'unknown';
334
+ if (index.byBranch.has(id)) return 'branch';
335
+ if (index.byMention.has(id)) return 'mention';
336
+ if (index.byChange && index.byChange.has(id)) return 'change';
337
+ return null;
338
+ }
339
+
340
+ /** The sentence a surface shows for an attribution, so no caller writes its own wording. */
341
+ function attributionEvidence(signal, id, extra) {
342
+ if (signal === 'branch') return `an initiative/${id} branch merged to the trunk`;
343
+ if (signal === 'mention') return 'a commit on the trunk names this initiative and changes build files';
344
+ if (signal === 'change') return 'a change merged to the trunk carried this initiative\'s own documents and build files together';
345
+ if (signal === 'declared') return `delivered_by ${String(extra || '').slice(0, 8)} — verified on the trunk, and it changes build paths`;
346
+ if (signal === 'unknown') return 'the trunk history could not be read — delivery cannot be derived here';
347
+ return 'no commit on the trunk is attributable to this initiative';
348
+ }
349
+
350
+ // Exported for tests that need a clean read after mutating a fixture's history in place.
351
+ function _resetCache() { deliveryCache.clear(); }
352
+
353
+ module.exports = {
354
+ deliveryIndex, attribution, attributionEvidence, declaredDelivery, mergedBranches,
355
+ isDeliveryPath, trunkRef, documentOwners, _resetCache,
356
+ };
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+ /* intake.js — the one-line intake run (planning-flows run 4). Pure: reads planning/out-of-scope/, writes nothing.
3
+ * Honest by design: confidence counts matched lane signals, the anchor is never invented, a prior rejection refuses. */
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { laneNames } = require('./lane-defs');
7
+
8
+ const SIGNALS = {
9
+ incident: ['outage', 'incident', 'postmortem', 'broke', 'down', 'sev1', 'sev2', 'regression in prod'],
10
+ discovery: ['investigate', 'explore', 'research', 'spike', 'unknown', 'should we', 'whether', 'feasibility', 'discover'],
11
+ chore: ['chore', 'bump', 'upgrade', 'rename', 'cleanup', 'clean up', 'refactor', 'dependency', 'dependencies', 'lint'],
12
+ content: ['blog', 'copy', 'docs', 'documentation', 'announcement', 'article', 'landing page copy', 'newsletter'],
13
+ prototype: ['prototype', 'mockup', 'sketch', 'specimen', 'try out', 'what if'],
14
+ feature: ['add', 'build', 'new', 'support', 'allow', 'let users', 'enable', 'feature', 'create', 'implement'],
15
+ };
16
+ const STOP = new Set('a an the and or of to in on for with is are be as at by it this that from into we i our my your should can will new add use using make get'.split(' '));
17
+
18
+ const words = (s) => String(s || '').toLowerCase().match(/[a-z0-9][a-z0-9-]*/g) || [];
19
+ const sig = (s) => new Set(words(s).filter((w) => w.length > 2 && !STOP.has(w)));
20
+
21
+ function parseIntakeLine(text) {
22
+ const title = String(text || '').trim().replace(/\s+/g, ' ');
23
+ const low = ` ${title.toLowerCase()} `;
24
+ const valid = new Set(laneNames());
25
+ const hits = {};
26
+ for (const [lane, keys] of Object.entries(SIGNALS)) {
27
+ if (!valid.has(lane)) continue;
28
+ const n = keys.filter((k) => new RegExp(`(^|[^a-z0-9])${k.replace(/ /g, '\\s+')}([^a-z0-9]|$)`).test(low)).length;
29
+ if (n) hits[lane] = n;
30
+ }
31
+ const ranked = Object.entries(hits).sort((a, b) => b[1] - a[1]);
32
+ if (!ranked.length) return { title, lane_guess: null, confidence: 0.2 };
33
+ const total = ranked.reduce((s, [, n]) => s + n, 0);
34
+ const [lane, n] = ranked[0];
35
+ // Honest: more matches and less competition raise it; the ceiling is 0.85 because a keyword match is not a judgment.
36
+ const confidence = Math.min(0.85, Math.round((0.3 + 0.15 * n) * (n / total) * 100) / 100 || 0.2);
37
+ return { title, lane_guess: lane, confidence };
38
+ }
39
+
40
+ function priorRejections(root, text) {
41
+ const dir = path.join(root, 'planning', 'out-of-scope');
42
+ let files = [];
43
+ try { files = fs.readdirSync(dir).filter((f) => f.endsWith('.md') && f !== 'README.md'); } catch (_) { return []; }
44
+ const want = sig(text);
45
+ const out = [];
46
+ for (const f of files) {
47
+ let body = '';
48
+ try { body = fs.readFileSync(path.join(dir, f), 'utf8'); } catch (_) { continue; }
49
+ const t = (body.match(/^title:\s*"?(.*?)"?\s*$/m) || [])[1] || f.replace(/\.md$/, '');
50
+ const topic = (body.match(/^topic:\s*"?(.*?)"?\s*$/m) || [])[1] || '';
51
+ const have = sig(`${t} ${topic} ${f.replace(/\.md$/, '').replace(/-/g, ' ')}`);
52
+ const shared = [...want].filter((w) => have.has(w));
53
+ if (shared.length >= 2) out.push({ file: `planning/out-of-scope/${f}`, title: t, shared });
54
+ }
55
+ return out;
56
+ }
57
+
58
+ // anchor: only an explicit `health:<id>` or `KR<n>` typed in the line; the caller must still confirm it.
59
+ function suggestAnchor(line) {
60
+ const m = String(line || '').match(/\bhealth:[a-z0-9-]+\b/i) || String(line || '').match(/\bKR\d+\b/);
61
+ return m ? m[0] : null;
62
+ }
63
+
64
+ function intakeDraft({ line, root, acknowledged = false }) {
65
+ const p = parseIntakeLine(line);
66
+ const prior = priorRejections(root || process.cwd(), line);
67
+ const refused = prior.length > 0 && !acknowledged;
68
+ return {
69
+ title: p.title, lane: p.lane_guess, confidence: p.confidence,
70
+ anchor_suggestion: suggestAnchor(line),
71
+ prior_rejections: prior,
72
+ refused,
73
+ refused_reason: refused ? `Prior rejection on record: ${prior.map((r) => r.title).join('; ')}. Acknowledge it before you create.` : null,
74
+ stage: 'discover',
75
+ };
76
+ }
77
+
78
+ module.exports = { parseIntakeLine, priorRejections, intakeDraft, suggestAnchor };
@@ -0,0 +1,226 @@
1
+ 'use strict';
2
+ /*
3
+ * Lane definitions — the one place a workflow is defined (D77).
4
+ *
5
+ * A lane is a kind of work. This file says, for each lane: what it is for, what goes in, which
6
+ * rungs the work passes, what comes out, and when it is done. The workflow engine, the lane
7
+ * picker, the lane write and the workspace detail pane all read THIS file. Change a lane here and
8
+ * every one of them changes; there is no second list to keep in step.
9
+ *
10
+ * Two kinds of field:
11
+ * · prose (label, when, input, output, done) — shown to the PM, never read by the engine's rules;
12
+ * · rules (track, rungs, labels) — read by the engine, so a change here changes the ladder.
13
+ *
14
+ * The engine runs under stock Node with no dependencies, and the kit vendors it by walking its
15
+ * relative requires, so this is a plain CommonJS module rather than a JSON file.
16
+ *
17
+ * `rungs` lists the pm-artifacts rungs a lane passes through. A rung left out reads `n/a` with the
18
+ * lane named as the reason, so a lane that skips a rung says so instead of showing it pending.
19
+ * The code profile's ladder is fixed by D56/D58/D60; its definitions here are descriptive.
20
+ */
21
+
22
+ const PM_ALL_RUNGS = ['brief', 'draft', 'review', 'approved', 'handoff'];
23
+ const PM_NO_HANDOFF = ['brief', 'draft', 'review', 'approved'];
24
+
25
+ const PM_ARTIFACTS = {
26
+ 'new-feature': {
27
+ label: 'New feature',
28
+ group: 'work',
29
+ when: 'A capability that does not exist yet.',
30
+ input: 'The problem, who has it, and the evidence that it is real.',
31
+ output: 'A product requirements document (PRD).',
32
+ done: 'The PRD exists, a reviewer has judged it, the PM has accepted it, and it is handed to the team or tracker.',
33
+ track: 'governed', rungs: PM_ALL_RUNGS, brief_label: 'problem brief', draft_label: 'PRD',
34
+ },
35
+ improvement: {
36
+ label: 'Feature improvement',
37
+ group: 'work',
38
+ when: 'A change to a feature that already exists.',
39
+ input: 'The feature as it is now, what is wrong or missing, and the measure the change should move.',
40
+ output: 'A change specification: what changes, what stays the same, and how to tell it worked.',
41
+ done: 'The change specification exists, a reviewer has judged it, the PM has accepted it, and it is handed off.',
42
+ track: 'governed', rungs: PM_ALL_RUNGS, brief_label: 'change brief', draft_label: 'change specification',
43
+ },
44
+ 'bug-fix': {
45
+ label: 'Bug fix',
46
+ group: 'work',
47
+ when: 'Behaviour that is wrong against what was promised.',
48
+ input: 'A bug report: steps to reproduce, expected result, actual result, severity.',
49
+ output: 'A fix specification: the cause, the behaviour after the fix, and the check that proves it.',
50
+ done: 'The fix specification exists, a reviewer has judged it, the PM has accepted it, and it is handed off.',
51
+ track: 'light', rungs: PM_ALL_RUNGS, brief_label: 'bug report', draft_label: 'fix specification',
52
+ },
53
+ discovery: {
54
+ label: 'New product discovery',
55
+ group: 'work',
56
+ when: 'A question that must be answered before anyone commits to build.',
57
+ input: 'The question, what is unknown, and the evidence that would make this worth picking up or dropping.',
58
+ output: 'Findings and a recommendation: go, drop, or learn more.',
59
+ done: 'The findings exist, a reviewer has judged them, the PM has accepted them, and the recommendation is handed off.',
60
+ track: 'governed', rungs: PM_ALL_RUNGS, brief_label: 'question brief', draft_label: 'findings',
61
+ },
62
+ chore: {
63
+ label: 'Chore',
64
+ group: 'work',
65
+ when: 'Upkeep that needs no requirements document.',
66
+ input: 'One paragraph that states the intent.',
67
+ output: 'The change itself, with the paragraph in the record.',
68
+ done: 'The change is made and the PM has accepted it. Nothing is handed off.',
69
+ track: 'light', rungs: PM_NO_HANDOFF, brief_label: 'intent', draft_label: 'change',
70
+ },
71
+ prd: {
72
+ label: 'PRD (document)',
73
+ group: 'document',
74
+ when: 'A standalone requirements document that is not a new feature, an improvement or a bug fix.',
75
+ input: 'The brief: the question, the reader, the decision it informs.',
76
+ output: 'A product requirements document.',
77
+ done: 'The PRD exists, a reviewer has judged it, the PM has accepted it, and it is handed off.',
78
+ track: 'governed', rungs: PM_ALL_RUNGS, brief_label: 'brief', draft_label: 'PRD',
79
+ },
80
+ strategy: {
81
+ label: 'Strategy (document)',
82
+ group: 'document',
83
+ when: 'A statement of direction: where to play, how to win, what not to do.',
84
+ input: 'The brief: the question, the reader, the decision it informs.',
85
+ output: 'A strategy document.',
86
+ done: 'The document exists, a reviewer has judged it, the PM has accepted it, and it is handed off.',
87
+ track: 'governed', rungs: PM_ALL_RUNGS, brief_label: 'brief', draft_label: 'strategy document',
88
+ },
89
+ 'project-plan': {
90
+ label: 'Project plan (document)',
91
+ group: 'document',
92
+ when: 'Who does what, by when, in what order.',
93
+ input: 'The scope, the people and the dates that are fixed.',
94
+ output: 'A project plan.',
95
+ done: 'The plan exists, a reviewer has judged it, the PM has accepted it, and it is handed off.',
96
+ track: 'governed', rungs: PM_ALL_RUNGS, brief_label: 'brief', draft_label: 'project plan',
97
+ },
98
+ 'decision-record': {
99
+ label: 'Decision record (document)',
100
+ group: 'document',
101
+ when: 'A decision that was made and must not be re-argued.',
102
+ input: 'The options, the choice, and the reason.',
103
+ output: 'A decision record.',
104
+ done: 'The record exists, a reviewer has judged it, and the PM has accepted it. Nothing is handed off.',
105
+ track: 'light', rungs: PM_NO_HANDOFF, brief_label: 'brief', draft_label: 'decision record',
106
+ },
107
+ report: {
108
+ label: 'Report (document)',
109
+ group: 'document',
110
+ when: 'An account of what happened or where things stand.',
111
+ input: 'The period or subject, and the reader.',
112
+ output: 'A report.',
113
+ done: 'The report exists, a reviewer has judged it, and the PM has accepted it. Nothing is handed off.',
114
+ track: 'light', rungs: PM_NO_HANDOFF, brief_label: 'brief', draft_label: 'report',
115
+ },
116
+ };
117
+
118
+ /*
119
+ * The code profile's lanes. They share ONE ladder; a lane changes only the artifact the spec rung
120
+ * requires (`requires`), whether a rubric is needed, the prototype rule, and the build-path rule. Those
121
+ * rules are fixed by D56/D58/D60 and live in workflow-state.js; what is here is what the PM reads.
122
+ */
123
+ const PRODUCT_CODE = {
124
+ feature: {
125
+ label: 'Feature',
126
+ group: 'work',
127
+ when: 'New or changed behaviour in the product.',
128
+ input: 'A product requirements document and an evaluation rubric, written before the build.',
129
+ requires: 'A PRD and a rubric, both written before the build.',
130
+ output: 'A pull request that satisfies the rubric.',
131
+ done: 'The build passes the rubric, an explainer is written, and the PM has accepted it.',
132
+ },
133
+ discovery: {
134
+ label: 'Discovery',
135
+ group: 'work',
136
+ when: 'A question to answer before building.',
137
+ input: 'A spike brief: the question and the evidence that would end the spike.',
138
+ requires: 'A spike brief.',
139
+ output: 'Findings that answer the question.',
140
+ done: 'The findings answer the brief and the PM has accepted them.',
141
+ },
142
+ chore: {
143
+ label: 'Chore',
144
+ group: 'work',
145
+ when: 'Upkeep that needs no requirements document.',
146
+ input: 'One paragraph that states the intent.',
147
+ requires: 'One paragraph of intent, written in the record\'s status.',
148
+ output: 'The change.',
149
+ done: 'The change is made and the PM has accepted it.',
150
+ },
151
+ incident: {
152
+ label: 'Incident',
153
+ group: 'work',
154
+ when: 'Something broke and was fixed first.',
155
+ input: 'A record written after the fix: what broke, the cause, the repair.',
156
+ requires: 'An incident record.',
157
+ output: 'The fix and its post-hoc record.',
158
+ done: 'The record states the cause and the repair, and the PM has accepted it.',
159
+ },
160
+ content: {
161
+ label: 'Content',
162
+ group: 'work',
163
+ when: 'Writing or media that ships with the product.',
164
+ input: 'A content brief. A rubric is recommended.',
165
+ requires: 'A content brief. A rubric is optional.',
166
+ output: 'The content.',
167
+ done: 'The content meets the brief and the PM has accepted it.',
168
+ },
169
+ prototype: {
170
+ label: 'Prototype',
171
+ group: 'work',
172
+ when: 'An idea the PM cannot judge from a description.',
173
+ input: 'A quick brief.',
174
+ requires: 'A quick prototype brief.',
175
+ output: '2–3 specimens the PM can look at. A prototype is never scored.',
176
+ done: 'The PM has looked at the specimens and chosen what to do with them.',
177
+ },
178
+ };
179
+
180
+ /*
181
+ * What decides each rung. This is the answer to "how does PMOS know this step is done" — one
182
+ * sentence per rung, shown beside the evidence in the detail pane. It restates the engine's test;
183
+ * the engine remains the authority, and the suite checks that every rung the engine emits has a
184
+ * sentence here.
185
+ */
186
+ const RUNG_RULES = {
187
+ 'pm-artifacts': {
188
+ route: 'The lane named in the record is one this repository offers.',
189
+ anchor: 'parent_kr names a KR, a declared health budget, or a domain goal that resolves.',
190
+ brief: 'The file named in brief_path (or prd_path) exists.',
191
+ draft: 'The file named in doc_path exists. On the governed track it did not first appear before the brief.',
192
+ review: 'evals.jsonl holds a verdict for this initiative, or the record names a reviewer in reviewed_by.',
193
+ approved: 'acceptances.jsonl holds an accepted line for this initiative. The PM records it; it is never automatic.',
194
+ handoff: 'handed_off in the record names where the work went.',
195
+ },
196
+ 'product-code': {
197
+ route: 'The lane is one this repository offers, and its specimen and surface questions are answered.',
198
+ anchor: 'parent_kr names a KR, a declared health budget, or a domain goal that resolves.',
199
+ prototype: 'A prototype brief first appeared before the PRD, where the specimen test calls for one.',
200
+ understand: 'An explainer for the existing surface first appeared before the PRD.',
201
+ spec: 'The PRD and rubric named in the record exist and first appeared at or before the first build commit.',
202
+ priors: 'The record cites a prior run or states that none applies.',
203
+ contract: 'A run in agent-runs.jsonl holds done criteria and a test method with PM_approved set.',
204
+ build: 'Commits attributed to this initiative change a build path.',
205
+ quality: 'CI decides on the pull request. The engine never claims a test result it did not run.',
206
+ review: 'evals.jsonl holds a Review-Gate verdict for this initiative, or a skip.',
207
+ explain: 'An explainer exists for the run.',
208
+ accept: 'acceptances.jsonl holds an accepted line for this initiative. The PM records it.',
209
+ outcome: 'outcome_checks holds a validated, refuted or inconclusive disposition with evidence.',
210
+ },
211
+ };
212
+
213
+ function defsFor(profile) {
214
+ return profile === 'pm-artifacts' ? PM_ARTIFACTS : PRODUCT_CODE;
215
+ }
216
+ function laneNames(profile) {
217
+ return Object.keys(defsFor(profile));
218
+ }
219
+ function laneDef(profile, lane) {
220
+ return defsFor(profile)[lane] || null;
221
+ }
222
+ function rungRule(profile, key) {
223
+ return (RUNG_RULES[profile] || {})[key] || null;
224
+ }
225
+
226
+ module.exports = { PM_ARTIFACTS, PRODUCT_CODE, RUNG_RULES, defsFor, laneNames, laneDef, rungRule };