@attalabs/vinaya 0.9.0 → 0.12.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/README.md +7 -5
- package/aeg-root/enforcement.md +3 -3
- package/aeg-root/glossary.md +4 -0
- package/aeg-root/roles/archivist.md +2 -2
- package/aeg-root/roles/developer.md +29 -5
- package/aeg-root/roles/planner.md +1 -1
- package/aeg-root/roles/reviewer.md +1 -1
- package/aeg-root/roles/security.md +1 -1
- package/aeg-root/roles/tranche-archivist.md +1 -1
- package/aeg-root/state-machine.md +6 -6
- package/aeg-root/templates/pr-report-template.md +8 -4
- package/aeg-root/tranche-model.md +33 -11
- package/dist/checks/bin/check-branch-topology.js +204 -115
- package/dist/checks/bin/check-brief-shape.js +204 -115
- package/dist/checks/bin/check-closes-n.js +204 -115
- package/dist/checks/bin/check-coherence.js +204 -115
- package/dist/checks/bin/check-dead-branch-push.js +204 -115
- package/dist/checks/bin/check-dispatch-readiness.js +204 -115
- package/dist/checks/bin/check-doc-coverage-push.js +231 -124
- package/dist/checks/bin/check-doc-coverage.js +233 -126
- package/dist/checks/bin/check-evidence-fresh.js +3173 -0
- package/dist/checks/bin/check-first-push-dispatch.js +204 -115
- package/dist/checks/bin/check-issue-assignment.js +204 -115
- package/dist/checks/bin/check-no-disk-state.js +204 -115
- package/dist/checks/bin/check-reader-resolvable-prose.js +204 -115
- package/dist/checks/bin/check-registry-gates.js +204 -115
- package/dist/checks/bin/check-review-gate.js +204 -115
- package/dist/checks/bin/check-single-plan-pr.js +204 -115
- package/dist/checks/bin/check-test-plan.js +204 -115
- package/dist/index.js +487 -221
- package/package.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/required-server-files.json +3 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js +2 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/1q96_modules_@clerk_nextjs_dist_esm_app-router_client_keyless-creator-reader_0lom2js.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__029-1ch._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__05t6482._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0gw1_g2._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0mdkphe._.js +3 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0plfbyn._.js +45 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0q56ohl._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0rr_lq7._.js +51 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__19jyp3m._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1_ojzp1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1d_8nm4._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1p19382._.js → [root-of-the-server]__1wsl6ro._.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1zkwtx8._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__210ehg9._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_00yjcv1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0rix02i._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_18-d2be._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +5 -4
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1gw6vc9._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_projects_[name]_tranches_[slug]_20yap_f._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3zyedaek9nvr_.js → 1eo32v1tbjy7c.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1t7yy_s34022p.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3antrj5z0uf_c.js → 2ieu4oi-pp_bz.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/server.js +1 -1
- package/templates/custom-check.template.ts +5 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1s7kj3s._.js +0 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1tswrm4._.js +0 -9
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_01jl99k._.js +0 -45
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_20c6-3m._.js +0 -45
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/2e68sbtdhi5dg.js +0 -1
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → ZHVE9Y9G8XKR91g27SgVX}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → ZHVE9Y9G8XKR91g27SgVX}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → ZHVE9Y9G8XKR91g27SgVX}/_ssgManifest.js +0 -0
|
@@ -260,32 +260,54 @@ Append-only. Each row records one role's turn at a phase. Re-entry appends a **n
|
|
|
260
260
|
|
|
261
261
|
- **Phase** — free-text. Convention: `<task-id>: <phase>` for per-task work (e.g. `9: develop`, `9: review`), or a bare phase for tranche-wide work (e.g. `planning`). Phase is opaque to the parser; the convention exists so a future view can pivot by task.
|
|
262
262
|
- **Role** — the AEG role doing the work (`Planner`, `Brief Author`, `Developer`, `Reviewer`, `Security`, `Archivist`).
|
|
263
|
-
- **Agent/Model** — the role's agent + model, with the surface in parentheses
|
|
263
|
+
- **Agent/Model** — free text: the role's agent + model, with a short tag for the host surface in parentheses (`claude-opus-4-7 (CC)` for a coding-agent session, `claude-opus-4-7 (chat)` for a conversational one — the tags the archived ledgers already use). Nothing parses the tag; it is there because the surface is what tells a reader which collection capability applied (below), and therefore whether a `—` cell was sanctioned.
|
|
264
264
|
- **Tokens in / Tokens out** — integers from the meter, or `—` for "not yet known."
|
|
265
|
-
- **Cost** — USD as `$X.XXXX
|
|
265
|
+
- **Cost** — USD as `$X.XXXX`, or `—`. *(V1 honesty: no maintained $/token pricing table for current models ships in this package, so `formatTokensLine` renders this cell as `—` on every row it emits. Historical rows may carry a `$X.XXXX` value supplied by hand or by an earlier table. Tokens are still exact; pricing is a known backlog dependency, not a ledger bug.)*
|
|
266
266
|
- **Date** — `YYYY-MM-DD`.
|
|
267
267
|
|
|
268
268
|
### The append rule (read this exactly the way you read derived status)
|
|
269
269
|
|
|
270
|
-
- **No role appends its own row on a task branch.** Two live-fire incidents forced this:
|
|
270
|
+
- **No role appends its own row on a task branch.** Two live-fire incidents forced this: read-only roles (Reviewer, Security, Planner, Brief Author) structurally cannot append — they hold no task branch, and some never touch the repo's filesystem at all; and parallel Developer sessions on different tasks collided appending to the same shared `tokens.md` file. Instead, every role **reports** its token spend in the artifact its turn already produces — the PR body ("Token report" section) for a role holding a branch, the verdict comment for a reviewing role, the plan PR or planning report for the Planner — and the per-task **Archivist appends every row at task close-out**, one row per role-turn, including its own.
|
|
271
271
|
- **Never edit** an existing row. If you discover a mistake, append a new row that supersedes it in prose (or fix the source file in a separate, declared edit — same exception that `state-machine.md` §13 carves for forward-reference fields).
|
|
272
272
|
- **Re-entry appends.** A Developer dispatched for `9: develop`, asked for changes, and re-running for `9: develop` again produces a **second** `9: develop` report, which the Archivist appends as a **second** row — never a sum, never an overwrite. The two rows both count.
|
|
273
273
|
- The **tranche total is `sum(rows)`**, derived at read time, never stored. This is the same philosophy as forge-derived status (don't store the aggregate; sum the immutable entries). A stored total reintroduces the merge-collision + stale-aggregate problem.
|
|
274
274
|
|
|
275
|
-
###
|
|
275
|
+
### Three layers — two portable, one host-specific
|
|
276
276
|
|
|
277
|
-
Token reporting is
|
|
277
|
+
Token reporting is three separable things, and only one of them depends on the agent host a role happens to run in. Keeping them apart is what lets this model be adopted on a harness AEG has never seen.
|
|
278
278
|
|
|
279
|
-
|
|
279
|
+
1. **The obligation — portable.** Every role reports its own turn's usage at turn-end. This does not weaken, is not best-effort, and is not conditional on the host making it convenient. It exists because the figure is otherwise unrecoverable after the fact (see the retraction below).
|
|
280
|
+
2. **Collection — host-specific.** *How* a role obtains true figures for its own turn. This is the only layer an adapter belongs in, and the only layer that may name a host, a transcript format, or a script path.
|
|
281
|
+
3. **The destination and the grammar — portable.** The `Tokens: …` line, in the artifact the role's turn already produced: the PR body's "Token report" section for a role that holds a branch, the verdict comment for a reviewing role, the plan PR or planning report for the Planner. The ledger stays append-only, and the Archivist transcribes what each role reported rather than re-deriving it.
|
|
280
282
|
|
|
281
|
-
|
|
282
|
-
- **claude.ai roles run in chat (Planner; Brief Author; Reviewer; Security).** A claude.ai conversation **cannot read its own token count** via tool or API, and has no transcript file the way a Claude Code session does — the reporter has nothing to run against. The role still reports at turn-end — phase, role, model, date, in its verdict comment or planning report — but leaves the numeric cells as `—`. The Archivist copies the report as-is; the **Principal** may later supply the real figures from the claude.ai UI usage figure, filling a previously-`—` cell (the one narrow forward-reference exception `state-machine.md` §13 allows).
|
|
283
|
+
The destination was never the coupled part — no role writes a shared ledger file on a task branch, so layer 3 is satisfied through the forge for every task-branch role. Two sources remain the known open gap recorded in `roles/archivist.md`: the Archivist's own row, and the Planner's tranche-wide report. **An adopter implements layer 2 for their own harness and inherits layers 1 and 3 unchanged.** A doctrine sentence that names a tool belongs in layer 2 or nowhere.
|
|
283
284
|
|
|
284
|
-
|
|
285
|
+
### Two collection capabilities (the design constraint, not a bug)
|
|
286
|
+
|
|
287
|
+
Token reporting is asymmetric — and any honest design has to encode that, because the asymmetry is a property of the **host's capability**, not of AEG and not of the role. The asymmetry lives in *what a role can collect*, not in *who writes the ledger file*: the Archivist writes every row, but the precision of the numbers it copies depends on whether the reporting role's host exposes usage to the agent at all.
|
|
288
|
+
|
|
289
|
+
The split is **operator vs. agent, not terminal vs. chat** — a figure a human reads off an interactive prompt or a usage dashboard is not reachable from an unattended agent session (dispatched, automated, no human at the keyboard), whatever kind of surface it runs on. (2026-08-08: retracted — an earlier revision of this section claimed a terminal role reports exact numbers "from `/cost`", an operator-typed slash command in one particular host; no agent session ever could, in that host or any other.)
|
|
290
|
+
|
|
291
|
+
- **Self-metering** — a role whose host exposes the running session's own usage to the agent itself, so the role can collect real figures with no operator step. Typically the Developer, and the Archivist when it runs as automation. The role collects through its host's layer-2 mechanism and reports the exact numbers in its PR body; the Archivist copies them verbatim into the row it appends at close-out. **A self-metering role may not report `—` in the `Tokens in` or `Tokens out` cells** — real figures, or the collection step is broken and *that* is what to report. The `Cost` cell is outside this rule: it reads `—` on every row the shipped renderer emits, for the pricing-table reason given above.
|
|
292
|
+
- **Operator-metered** — a role whose host exposes no usage to the agent, leaving a human the only source of the figure. Typically the Planner, Brief Author, Reviewer and Security, which run in conversational surfaces with no session record the agent can read and no meter it can query. The role **still reports at turn-end** — phase, role, model, date, in its verdict comment or planning report — and leaves the numeric cells as `—` when no figure reached it. The blank is licensed by the **absence of a figure**, not by the role's label: where an operator hands the role real numbers at report time (a route layer 2 explicitly allows), those numbers go in the cells and `—` is not correct. The Archivist copies the report as-is; the **Principal** may later supply the real figures from whatever usage view the host does offer a human, filling a previously-`—` cell (the one narrow forward-reference exception `state-machine.md` §13 allows).
|
|
293
|
+
|
|
294
|
+
**Per-cell optionality is conditioned on host capability, never on convenience.** `—` in a **token** cell is sanctioned in exactly one situation: the host cannot expose the figure to the agent. (The **Cost** cell is separate and is always `—` while this package carries no maintained $/token table — see the Cost bullet above.) It is never a shortcut for a role that could have collected the number, and no role ever estimates a figure or fills in another role's cell — an invented number in an append-only ledger is worse than an honest unknown, because nothing downstream can tell the two apart.
|
|
295
|
+
|
|
296
|
+
Which capability applies is a fact about the **host**, not a fixed property of the role: the same role is self-metering on a harness that exposes usage and operator-metered on one that does not. Read the role names above as today's common case, not as an allocation.
|
|
297
|
+
|
|
298
|
+
Self-metering capture is real, not estimated: the adapter sums the session's own usage records, so a Developer or automated Archivist turn's numbers are exact rather than approximated. Whether it also runs with *no* operator step depends on the host's wiring — where a Stop hook writes a transcript pointer — configured in the repo or in the operator's own host settings — the adapter finds it unaided; with no pointer written, the caller names the transcript with `--transcript <path>`. Either way the figures are read, never guessed. The remaining manual seam is operator-metered roles only — closing it depends on the host giving a session a way to read its own usage, which conversational surfaces generally do not today. **Known gap (flagged, not solved):** tranche-wide operator-metered turns with no task PR to report into — a Planner session outside a plan PR, a Brief Author session — have no established recording path; see `roles/planner.md` "Plan-PR close-out."
|
|
299
|
+
|
|
300
|
+
### The collection adapter AEG ships (one layer-2 instance, not the requirement)
|
|
301
|
+
|
|
302
|
+
AEG ships exactly one collection adapter, for the Claude Code host: `packages/aeg-core/bin/report-tokens.ts`. It reads that host's session transcript — a JSONL file carrying a `usage` object per assistant message — sums it, and emits a layer-3 `Tokens: …` line. Every doctrine citation of it is an **example of one way to satisfy layer 2**, never the requirement; a role on another host satisfies the same obligation by other means and is equally compliant.
|
|
303
|
+
|
|
304
|
+
**The seam is the summary shape, not the file tree.** An adapter's whole job is to produce a `TranscriptSummary` — four integers (fresh input, cache creation, cache read, output) plus a model id. Everything downstream of that shape is portable and already shipped: `formatTokensLine` renders the grammar, `parse-token-report.ts` reads it back. Two things sit adapter-side because both know host-specific facts: `summarizeTranscript` (`packages/aeg-core/src/claude-code-transcript.ts`), which knows Claude Code's transcript JSONL and its `usage` field names, and the bin itself, which knows how that host points a session at its own transcript. The `bin/` vs `src/` split does **not** mark this seam and never did.
|
|
305
|
+
|
|
306
|
+
**To satisfy layer 2 on another harness:** obtain your turn's usage figures by whatever means your host offers — its own transcript or log, an API usage response, a meter the harness exposes, or an operator reading them off a dashboard and handing them over — and write the layer-3 line yourself. Reusing this adapter is not required, and neither is writing any code: the requirement is the reported figures, in the grammar, in the right artifact. If your host exposes nothing to the agent, you are operator-metered and the `—` rule above applies — that is the sanctioned outcome, not a failure to comply.
|
|
285
307
|
|
|
286
308
|
### Live reads (Studio) — a second, narrower read path
|
|
287
309
|
|
|
288
|
-
Vinaya Studio's tranche page no longer reads `<name>.tokens.md` off disk to render token totals — it re-derives the same row shape live off the forge: every MERGED PR on a task's own branch (`task/<tranche>/<id>`), parsing the Developer's "Token report" entries from the PR body (every one, including re-push entries) and the Reviewer's/Security's `Tokens: …` lines from that PR's comments (`packages/aeg-core/src/parse-token-report.ts`'s `aggregateTaskTokenRows`, fetched by
|
|
310
|
+
Vinaya Studio's tranche page no longer reads `<name>.tokens.md` off disk to render token totals — it re-derives the same row shape live off the forge: every MERGED PR on a task's own branch (`task/<tranche>/<id>`), parsing the Developer's "Token report" entries from the PR body (every one, including re-push entries) and the Reviewer's/Security's `Tokens: …` lines from that PR's comments (`packages/aeg-core/src/parse-token-report.ts`'s `aggregateTaskTokenRows`, fetched by Studio's own forge-reading layer in the repo that hosts it). Same row shape, same `sumLedger` totals math (`parse-ledger.ts`) — different source.
|
|
289
311
|
|
|
290
312
|
This is deliberately **narrower** than what the Archivist collects into the file: it cannot recover the Archivist's own `<task-id>: archive` row (no PR carries it — the file itself is that row's only record) or the Planner's `Tokens: planning …` report (no reliable way to attribute a plan PR to one task from the forge alone without false-positive cross-task matches, confirmed live during 4b's build — a task Issue's cross-reference timeline picks up ANY PR that merely mentions its number in passing prose, not just its own plan PR). Both remain recoverable only from `.tokens.md`, which is why the file is not deleted here (task 7's job, once the live mechanism is proven in wider use). A task's report that's missing or malformed (e.g. a "Token report" heading with no table and no parseable text after it) yields no row for that report, never a fabricated one — same discipline as the Archivist's own DANGLING convention.
|
|
291
313
|
|
|
@@ -294,7 +316,7 @@ This is deliberately **narrower** than what the Archivist collects into the file
|
|
|
294
316
|
The ledger is a Section-13 append-only artifact. The familiar forbidden moves apply:
|
|
295
317
|
|
|
296
318
|
- **No stored total.** Do not add a "current total" row, header field, or anything that has to be edited when a row appends. Derive it.
|
|
297
|
-
- **No edits to past rows** (except the forward-reference exception in §13: filling a previously `—` numeric cell from the
|
|
319
|
+
- **No edits to past rows** (except the forward-reference exception in §13: the Principal filling a previously `—` numeric cell from whatever usage view the host offers a human is permitted — it does not change history, only completes it).
|
|
298
320
|
- **No "current spend"** field anywhere — including the tranche file's header. The forge holds execution state; the ledger file holds the cost history. The thin tranche file holds topology. Three artifacts, three concerns.
|
|
299
321
|
|
|
300
322
|
---
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/checks/bin/check-branch-topology.ts
|
|
4
4
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
5
|
-
// ../../packages/aeg-
|
|
5
|
+
// ../../packages/aeg-forge-state/src/strip-code.ts
|
|
6
6
|
function maskCode(body) {
|
|
7
7
|
const fill = (line) => " ".repeat(line.length);
|
|
8
8
|
return maskIndentedCode(maskFencedCode(body, fill), fill).replace(/(`+)[^\n]*?\1/g, (m) => " ".repeat(m.length));
|
|
@@ -19,29 +19,38 @@ function stripCode(body, options = {}) {
|
|
|
19
19
|
var FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})([^\r\n]*)\r?$/;
|
|
20
20
|
var FENCE_CLOSE = /^ {0,3}(`+|~+)[ \t]*\r?$/;
|
|
21
21
|
function maskFencedCode(body, fill) {
|
|
22
|
+
const { lines } = scanFencedCode(body);
|
|
23
|
+
return lines.map(({ line, inCode }) => inCode ? fill(line) : line).join(`
|
|
24
|
+
`);
|
|
25
|
+
}
|
|
26
|
+
function scanFencedCode(body) {
|
|
22
27
|
let fenceChar = null;
|
|
23
28
|
let fenceLen = 0;
|
|
24
|
-
|
|
29
|
+
const lines = body.split(`
|
|
25
30
|
`).map((line) => {
|
|
26
31
|
if (fenceChar === null) {
|
|
27
32
|
const open = line.match(FENCE_OPEN);
|
|
28
33
|
if (!open)
|
|
29
|
-
return line;
|
|
34
|
+
return { line, inCode: false };
|
|
30
35
|
const marker = open[1];
|
|
31
36
|
if (marker[0] === "`" && open[2].includes("`"))
|
|
32
|
-
return line;
|
|
37
|
+
return { line, inCode: false };
|
|
33
38
|
fenceChar = marker[0];
|
|
34
39
|
fenceLen = marker.length;
|
|
35
|
-
return
|
|
40
|
+
return { line, inCode: true };
|
|
36
41
|
}
|
|
37
42
|
const close = line.match(FENCE_CLOSE);
|
|
38
43
|
if (close && close[1][0] === fenceChar && close[1].length >= fenceLen) {
|
|
39
44
|
fenceChar = null;
|
|
40
45
|
fenceLen = 0;
|
|
41
46
|
}
|
|
42
|
-
return
|
|
43
|
-
})
|
|
44
|
-
|
|
47
|
+
return { line, inCode: true };
|
|
48
|
+
});
|
|
49
|
+
return { lines, unterminated: fenceChar !== null };
|
|
50
|
+
}
|
|
51
|
+
function hasUnterminatedFence(body) {
|
|
52
|
+
return scanFencedCode(body.replace(/\r\n?/g, `
|
|
53
|
+
`)).unterminated;
|
|
45
54
|
}
|
|
46
55
|
var LIST_MARKER = /^ {0,3}(?:[-*+]|\d{1,9}[.)])(?:\s|$)/;
|
|
47
56
|
function indentWidth(line) {
|
|
@@ -84,88 +93,7 @@ function maskIndentedCode(body, fill) {
|
|
|
84
93
|
return out.join(`
|
|
85
94
|
`);
|
|
86
95
|
}
|
|
87
|
-
|
|
88
|
-
const masked = maskCode(body);
|
|
89
|
-
const start = new RegExp(`<!--\\s*AEG:${field}:START\\s*-->`).exec(masked);
|
|
90
|
-
if (!start)
|
|
91
|
-
return null;
|
|
92
|
-
const afterStart = start.index + start[0].length;
|
|
93
|
-
const end = new RegExp(`<!--\\s*AEG:${field}:END\\s*-->`).exec(masked.slice(afterStart));
|
|
94
|
-
if (!end)
|
|
95
|
-
return null;
|
|
96
|
-
return body.slice(afterStart, afterStart + end.index);
|
|
97
|
-
}
|
|
98
|
-
// ../../packages/aeg-core/src/parse-registry.ts
|
|
99
|
-
function parseRegistry(md) {
|
|
100
|
-
const rows = readMarkdownTable(md, /^##\s+Registry\b/i);
|
|
101
|
-
const projects = [];
|
|
102
|
-
for (const row of rows) {
|
|
103
|
-
if (row.length < 4)
|
|
104
|
-
continue;
|
|
105
|
-
const name = stripBackticks(row[0] ?? "").trim();
|
|
106
|
-
const path = stripBackticks(row[1] ?? "").trim();
|
|
107
|
-
const specsPath = stripBackticks(row[2] ?? "").trim();
|
|
108
|
-
const stateCell = (row[3] ?? "").trim();
|
|
109
|
-
if (!name || !path || !specsPath)
|
|
110
|
-
continue;
|
|
111
|
-
projects.push({
|
|
112
|
-
name,
|
|
113
|
-
path,
|
|
114
|
-
specsPath,
|
|
115
|
-
statePath: parseStateCell(stateCell)
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
return projects;
|
|
119
|
-
}
|
|
120
|
-
function parseStateCell(cell) {
|
|
121
|
-
if (!cell)
|
|
122
|
-
return null;
|
|
123
|
-
const stripped = stripBackticks(cell).trim();
|
|
124
|
-
if (/^[\w./@-]+\/?$/.test(stripped) && cell.includes("`")) {
|
|
125
|
-
return stripped;
|
|
126
|
-
}
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
function stripBackticks(s) {
|
|
130
|
-
return s.replace(/`/g, "");
|
|
131
|
-
}
|
|
132
|
-
function readMarkdownTable(md, headingMatcher) {
|
|
133
|
-
const lines = md.split(/\r?\n/);
|
|
134
|
-
let i = 0;
|
|
135
|
-
for (;i < lines.length; i++) {
|
|
136
|
-
if (headingMatcher.test(lines[i] ?? "")) {
|
|
137
|
-
i++;
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
if (i >= lines.length)
|
|
142
|
-
return [];
|
|
143
|
-
for (;i < lines.length; i++) {
|
|
144
|
-
if ((lines[i] ?? "").trim().startsWith("|"))
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
if (i + 1 >= lines.length)
|
|
148
|
-
return [];
|
|
149
|
-
i += 2;
|
|
150
|
-
const out = [];
|
|
151
|
-
for (;i < lines.length; i++) {
|
|
152
|
-
const trimmed = (lines[i] ?? "").trim();
|
|
153
|
-
if (!trimmed.startsWith("|"))
|
|
154
|
-
break;
|
|
155
|
-
out.push(splitTableRow(trimmed));
|
|
156
|
-
}
|
|
157
|
-
return out;
|
|
158
|
-
}
|
|
159
|
-
function splitTableRow(row) {
|
|
160
|
-
const PLACEHOLDER = "\x00PIPE\x00";
|
|
161
|
-
const escaped = row.replace(/\\\|/g, PLACEHOLDER);
|
|
162
|
-
const parts = escaped.split("|").map((s) => s.trim());
|
|
163
|
-
if (parts.length && parts[0] === "")
|
|
164
|
-
parts.shift();
|
|
165
|
-
if (parts.length && parts[parts.length - 1] === "")
|
|
166
|
-
parts.pop();
|
|
167
|
-
return parts.map((p) => p.split(PLACEHOLDER).join("|"));
|
|
168
|
-
}
|
|
96
|
+
|
|
169
97
|
// ../../packages/aeg-forge-state/src/parse-rationale-deps.ts
|
|
170
98
|
var SECTION_HEADER = /\*\*Dependency rationale\*\*/i;
|
|
171
99
|
var NEXT_HEADER = /\*\*[A-Z][^*]*\*\*/;
|
|
@@ -463,11 +391,37 @@ function findTrancheSlug(names) {
|
|
|
463
391
|
var TITLE_PATTERN = /^\[([^\]]+)]\s*(\S+)\s*—\s*(.+)$/;
|
|
464
392
|
var PROJECT_SLUG = /^[a-z0-9][a-z0-9-]*$/i;
|
|
465
393
|
var PROJECT_FIELD = /^\s*(?:\*\*)?Project(?:\(s\))?(?:\*\*)?\s*:\s*(?:\*\*)?\s*(.+)$/im;
|
|
394
|
+
var VALUE_EDGE_MARKUP = /^[`.;\s]+|[`.;\s]+$/g;
|
|
395
|
+
function unwrapValue(raw) {
|
|
396
|
+
return raw.replace(/\*\*/g, "").replace(VALUE_EDGE_MARKUP, "");
|
|
397
|
+
}
|
|
398
|
+
function projectFieldFromBody(body) {
|
|
399
|
+
const m = stripCode(body, { inlineSpans: "keep" }).match(PROJECT_FIELD);
|
|
400
|
+
if (m)
|
|
401
|
+
return parseFieldValue(m[1] ?? "");
|
|
402
|
+
if (hasUnterminatedFence(body)) {
|
|
403
|
+
const raw = body.match(PROJECT_FIELD);
|
|
404
|
+
if (raw)
|
|
405
|
+
return { declared: true, names: [], unparsed: [(raw[1] ?? "").trim()], unreadable: true };
|
|
406
|
+
}
|
|
407
|
+
return { declared: false, names: [], unparsed: [], unreadable: false };
|
|
408
|
+
}
|
|
409
|
+
function parseFieldValue(value) {
|
|
410
|
+
const names = [];
|
|
411
|
+
const unparsed = [];
|
|
412
|
+
for (const raw of value.split(",")) {
|
|
413
|
+
const name = unwrapValue(raw);
|
|
414
|
+
if (name.length === 0)
|
|
415
|
+
continue;
|
|
416
|
+
if (PROJECT_SLUG.test(name))
|
|
417
|
+
names.push(name);
|
|
418
|
+
else
|
|
419
|
+
unparsed.push(raw.trim());
|
|
420
|
+
}
|
|
421
|
+
return { declared: true, names: [...new Set(names)], unparsed, unreadable: false };
|
|
422
|
+
}
|
|
466
423
|
function projectsFromBody(body) {
|
|
467
|
-
|
|
468
|
-
if (!m)
|
|
469
|
-
return [];
|
|
470
|
-
return (m[1] ?? "").split(",").map((s) => s.trim()).filter((s) => PROJECT_SLUG.test(s));
|
|
424
|
+
return projectFieldFromBody(body).names;
|
|
471
425
|
}
|
|
472
426
|
function taskFromIssue(issue) {
|
|
473
427
|
const m = issue.title.match(TITLE_PATTERN);
|
|
@@ -509,8 +463,8 @@ function tasksFromIssues(issues) {
|
|
|
509
463
|
}
|
|
510
464
|
return tasks.sort((a, b) => compareTaskIds(a.id, b.id));
|
|
511
465
|
}
|
|
512
|
-
async function
|
|
513
|
-
return
|
|
466
|
+
async function fetchTrancheIssuesAsync(owner, repo, slug) {
|
|
467
|
+
return ghIssueListByAnyLabelAsync(owner, repo, [trancheLabel(slug)]);
|
|
514
468
|
}
|
|
515
469
|
function resolveTaskIssueRef(title, labels) {
|
|
516
470
|
const m = title.match(TITLE_PATTERN);
|
|
@@ -526,17 +480,20 @@ function resolveTaskIssueRef(title, labels) {
|
|
|
526
480
|
}
|
|
527
481
|
|
|
528
482
|
// ../../packages/aeg-forge-state/src/derive-from-forge.ts
|
|
529
|
-
|
|
530
|
-
const milestone = known ?? findMilestoneForSlug(owner, repo, slug);
|
|
531
|
-
const tasks = await listTasksForSlugAsync(owner, repo, slug);
|
|
483
|
+
function trancheFromIssues(slug, issues, known) {
|
|
532
484
|
return {
|
|
533
485
|
name: slug,
|
|
534
|
-
lifecycle:
|
|
535
|
-
goal:
|
|
536
|
-
tasks,
|
|
486
|
+
lifecycle: known?.lifecycle ?? "active",
|
|
487
|
+
goal: known?.goal ?? "",
|
|
488
|
+
tasks: tasksFromIssues(issues),
|
|
537
489
|
backlog: []
|
|
538
490
|
};
|
|
539
491
|
}
|
|
492
|
+
async function deriveTrancheFromForge(owner, repo, slug, known) {
|
|
493
|
+
const milestone = known ?? findMilestoneForSlug(owner, repo, slug);
|
|
494
|
+
const issues = await fetchTrancheIssuesAsync(owner, repo, slug);
|
|
495
|
+
return trancheFromIssues(slug, issues, milestone);
|
|
496
|
+
}
|
|
540
497
|
// ../../node_modules/universal-user-agent/index.js
|
|
541
498
|
function getUserAgent() {
|
|
542
499
|
if (typeof navigator === "object" && "userAgent" in navigator) {
|
|
@@ -1456,6 +1413,96 @@ var systemEnv3 = {
|
|
|
1456
1413
|
...process.env,
|
|
1457
1414
|
PATH: [process.env.PATH, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].filter(Boolean).join(":")
|
|
1458
1415
|
};
|
|
1416
|
+
// ../../packages/aeg-core/src/anchored-region.ts
|
|
1417
|
+
function anchoredRegionBounds(body, field) {
|
|
1418
|
+
const masked = maskCode(body);
|
|
1419
|
+
const start = new RegExp(`<!--\\s*AEG:${field}:START\\s*-->`).exec(masked);
|
|
1420
|
+
if (!start)
|
|
1421
|
+
return null;
|
|
1422
|
+
const innerStart = start.index + start[0].length;
|
|
1423
|
+
const end = new RegExp(`<!--\\s*AEG:${field}:END\\s*-->`).exec(masked.slice(innerStart));
|
|
1424
|
+
if (!end)
|
|
1425
|
+
return null;
|
|
1426
|
+
const innerEnd = innerStart + end.index;
|
|
1427
|
+
return { outerStart: start.index, outerEnd: innerEnd + end[0].length, innerStart, innerEnd };
|
|
1428
|
+
}
|
|
1429
|
+
function anchoredRegion(body, field) {
|
|
1430
|
+
const bounds = anchoredRegionBounds(body, field);
|
|
1431
|
+
if (!bounds)
|
|
1432
|
+
return null;
|
|
1433
|
+
return body.slice(bounds.innerStart, bounds.innerEnd);
|
|
1434
|
+
}
|
|
1435
|
+
// ../../packages/aeg-core/src/parse-registry.ts
|
|
1436
|
+
function parseRegistry(md) {
|
|
1437
|
+
const rows = readMarkdownTable(md, /^##\s+Registry\b/i);
|
|
1438
|
+
const projects = [];
|
|
1439
|
+
for (const row of rows) {
|
|
1440
|
+
if (row.length < 4)
|
|
1441
|
+
continue;
|
|
1442
|
+
const name = stripBackticks(row[0] ?? "").trim();
|
|
1443
|
+
const path = stripBackticks(row[1] ?? "").trim();
|
|
1444
|
+
const specsPath = stripBackticks(row[2] ?? "").trim();
|
|
1445
|
+
const stateCell = (row[3] ?? "").trim();
|
|
1446
|
+
if (!name || !path || !specsPath)
|
|
1447
|
+
continue;
|
|
1448
|
+
projects.push({
|
|
1449
|
+
name,
|
|
1450
|
+
path,
|
|
1451
|
+
specsPath,
|
|
1452
|
+
statePath: parseStateCell(stateCell)
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
return projects;
|
|
1456
|
+
}
|
|
1457
|
+
function parseStateCell(cell) {
|
|
1458
|
+
if (!cell)
|
|
1459
|
+
return null;
|
|
1460
|
+
const stripped = stripBackticks(cell).trim();
|
|
1461
|
+
if (/^[\w./@-]+\/?$/.test(stripped) && cell.includes("`")) {
|
|
1462
|
+
return stripped;
|
|
1463
|
+
}
|
|
1464
|
+
return null;
|
|
1465
|
+
}
|
|
1466
|
+
function stripBackticks(s) {
|
|
1467
|
+
return s.replace(/`/g, "");
|
|
1468
|
+
}
|
|
1469
|
+
function readMarkdownTable(md, headingMatcher) {
|
|
1470
|
+
const lines = md.split(/\r?\n/);
|
|
1471
|
+
let i = 0;
|
|
1472
|
+
for (;i < lines.length; i++) {
|
|
1473
|
+
if (headingMatcher.test(lines[i] ?? "")) {
|
|
1474
|
+
i++;
|
|
1475
|
+
break;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
if (i >= lines.length)
|
|
1479
|
+
return [];
|
|
1480
|
+
for (;i < lines.length; i++) {
|
|
1481
|
+
if ((lines[i] ?? "").trim().startsWith("|"))
|
|
1482
|
+
break;
|
|
1483
|
+
}
|
|
1484
|
+
if (i + 1 >= lines.length)
|
|
1485
|
+
return [];
|
|
1486
|
+
i += 2;
|
|
1487
|
+
const out = [];
|
|
1488
|
+
for (;i < lines.length; i++) {
|
|
1489
|
+
const trimmed = (lines[i] ?? "").trim();
|
|
1490
|
+
if (!trimmed.startsWith("|"))
|
|
1491
|
+
break;
|
|
1492
|
+
out.push(splitTableRow(trimmed));
|
|
1493
|
+
}
|
|
1494
|
+
return out;
|
|
1495
|
+
}
|
|
1496
|
+
function splitTableRow(row) {
|
|
1497
|
+
const PLACEHOLDER = "\x00PIPE\x00";
|
|
1498
|
+
const escaped = row.replace(/\\\|/g, PLACEHOLDER);
|
|
1499
|
+
const parts = escaped.split("|").map((s) => s.trim());
|
|
1500
|
+
if (parts.length && parts[0] === "")
|
|
1501
|
+
parts.shift();
|
|
1502
|
+
if (parts.length && parts[parts.length - 1] === "")
|
|
1503
|
+
parts.pop();
|
|
1504
|
+
return parts.map((p) => p.split(PLACEHOLDER).join("|"));
|
|
1505
|
+
}
|
|
1459
1506
|
// ../../packages/aeg-core/src/state-machine-model.ts
|
|
1460
1507
|
var FORGE_FACT_INPUTS = [
|
|
1461
1508
|
{
|
|
@@ -1959,23 +2006,65 @@ function checkIssueRationale(body) {
|
|
|
1959
2006
|
function isTaskIssueLabelSet(labels) {
|
|
1960
2007
|
return hasLabel("tranche", labels);
|
|
1961
2008
|
}
|
|
2009
|
+
function isControlCodePoint(codePoint) {
|
|
2010
|
+
return codePoint <= 31 || codePoint >= 127 && codePoint <= 159;
|
|
2011
|
+
}
|
|
2012
|
+
function isDisplayControlCodePoint(codePoint) {
|
|
2013
|
+
return codePoint >= 8203 && codePoint <= 8207 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8294 && codePoint <= 8297 || codePoint === 65279;
|
|
2014
|
+
}
|
|
2015
|
+
var MESSAGE_VALUE_COUNT_MAX = 5;
|
|
2016
|
+
function residueForMessage(values) {
|
|
2017
|
+
const shown = values.slice(0, MESSAGE_VALUE_COUNT_MAX).map((v) => `\`${forMessage(v)}\``);
|
|
2018
|
+
const rest = values.length - shown.length;
|
|
2019
|
+
return rest > 0 ? `${shown.join(", ")} and ${rest} more` : shown.join(", ");
|
|
2020
|
+
}
|
|
2021
|
+
var MESSAGE_VALUE_MAX = 64;
|
|
2022
|
+
function forMessage(value) {
|
|
2023
|
+
const kept = [];
|
|
2024
|
+
for (const ch of value.replace(/\s+/g, " ")) {
|
|
2025
|
+
const codePoint = ch.codePointAt(0) ?? 0;
|
|
2026
|
+
if (isControlCodePoint(codePoint) || isDisplayControlCodePoint(codePoint))
|
|
2027
|
+
continue;
|
|
2028
|
+
kept.push(ch);
|
|
2029
|
+
}
|
|
2030
|
+
const cleaned = kept.join("").trim();
|
|
2031
|
+
if (cleaned.length === 0)
|
|
2032
|
+
return "(unprintable)";
|
|
2033
|
+
const points = Array.from(cleaned);
|
|
2034
|
+
return points.length > MESSAGE_VALUE_MAX ? `${points.slice(0, MESSAGE_VALUE_MAX).join("")}…` : cleaned;
|
|
2035
|
+
}
|
|
1962
2036
|
function checkProjectsRegistered(body, _labels, registeredNames) {
|
|
1963
2037
|
if (registeredNames.length === 0)
|
|
1964
2038
|
return { status: "pass", errors: [] };
|
|
1965
|
-
const
|
|
1966
|
-
|
|
1967
|
-
if (unregistered.length === 0)
|
|
2039
|
+
const field = projectFieldFromBody(body);
|
|
2040
|
+
if (!field.declared)
|
|
1968
2041
|
return { status: "pass", errors: [] };
|
|
1969
|
-
const
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2042
|
+
const known = new Set(registeredNames.map((n) => n.trim()));
|
|
2043
|
+
const registeredList = [...registeredNames].map(forMessage).join(", ");
|
|
2044
|
+
const errors = [];
|
|
2045
|
+
if (field.unreadable) {
|
|
2046
|
+
return {
|
|
2047
|
+
status: "fail",
|
|
2048
|
+
errors: [
|
|
2049
|
+
`issue-validation project registry: the body has an unterminated code fence, so its \`**Project:**\` field could not be read reliably — the fence runs to end of body (CommonMark, and how the forge renders it), swallowing everything after it including the foot field where the declaration lives by convention. Read from the raw body the field says ${residueForMessage(field.unparsed) || "(nothing)"}, but a line inside a swallowed region may be a quoted example rather than a real declaration, so this gate refuses rather than guessing which. Balance the fences — every opening run of backticks or tildes needs a closing run at least as long — and the field reads normally.`
|
|
2050
|
+
]
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
const unregistered = field.names.filter((p) => !known.has(p));
|
|
2054
|
+
if (unregistered.length > 0) {
|
|
2055
|
+
const caseHints = unregistered.map((p) => {
|
|
2056
|
+
const row = [...known].find((k) => k.toLowerCase() === p.toLowerCase());
|
|
2057
|
+
return row ? `\`${forMessage(p)}\` differs from the registered \`${forMessage(row)}\` only in case` : null;
|
|
2058
|
+
}).filter((h) => h !== null);
|
|
2059
|
+
errors.push(`issue-validation project registry: the \`**Project:**\` field declares ${unregistered.map(forMessage).join(", ")} — no such row in \`.vinaya/projects.md\`, which is the authority for valid project names (registered: ${registeredList}). Fix the name, or register the project with \`vinaya init product <name> --path <folder>\` first; an unregistered project has no specs to read and no per-project state to update.${caseHints.length > 0 ? ` Note: ${caseHints.join("; ")} — project names are matched exactly, because every downstream consumer compares them literally.` : ""} This reads the same field \`projectsFromBody\` derives the task's project from, so a name here that is not a row is a task that resolves to a project that does not exist.`);
|
|
2060
|
+
}
|
|
2061
|
+
if (field.names.length === 0) {
|
|
2062
|
+
const residue = field.unparsed.length > 0 ? ` — ${residueForMessage(field.unparsed)}` : " — the field is empty";
|
|
2063
|
+
errors.push(`issue-validation project registry: the \`**Project:**\` field is present but resolves to no project name${residue} (registered: ${registeredList}). A project name is a slug (\`[a-z0-9][a-z0-9-]*\`, matched exactly); prose, a parenthetical, or a sentence in this field resolves to no project at all, and this gate cannot check a name it never receives — which is how a declaration like this used to pass. Write the registered name on its own, or register the project with \`vinaya init product <name> --path <folder>\` first. If the task genuinely touches no registered project, omit the field rather than explaining its absence inside it — \`checkIssueRationale\` already requires the \`Project(s) + blast radius\` narrative field for that.`);
|
|
2064
|
+
} else if (field.unparsed.length > 0) {
|
|
2065
|
+
errors.push(`issue-validation project registry: the \`**Project:**\` field declares a value that is not a project name — ${residueForMessage(field.unparsed)} (registered: ${registeredList}). The rest of the field parsed, so this is a name the gate silently could not check rather than a field it could not read at all. A project name is a slug (\`[a-z0-9][a-z0-9-]*\`, matched exactly); write it on its own, or drop it if it names no project.`);
|
|
2066
|
+
}
|
|
2067
|
+
return errors.length > 0 ? { status: "fail", errors } : { status: "pass", errors: [] };
|
|
1979
2068
|
}
|
|
1980
2069
|
|
|
1981
2070
|
// ../../packages/aeg-core/src/coherence-checks.ts
|