@attalabs/vinaya 0.22.0 → 0.24.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 +14 -3
- package/aeg-root/contracts/archivist-tranche-archivist.md +2 -2
- package/aeg-root/contracts/brief-developer.md +1 -0
- package/aeg-root/contracts/developer-reviewer.md +3 -3
- package/aeg-root/contracts/reviewer-archivist.md +3 -3
- package/aeg-root/enforcement.md +26 -20
- package/aeg-root/process.md +8 -4
- package/aeg-root/roles/archivist.md +2 -2
- package/aeg-root/roles/brief-author.md +9 -3
- package/aeg-root/roles/developer.md +63 -31
- package/aeg-root/roles/planner.md +1 -1
- package/aeg-root/roles/principal.md +14 -0
- package/aeg-root/roles/reviewer.md +32 -15
- package/aeg-root/roles/security.md +15 -5
- package/aeg-root/skills/aeg/SKILL.md +9 -5
- package/aeg-root/skills/aeg-roles/SKILL.md +2 -2
- package/aeg-root/skills/brief-authoring/SKILL.md +39 -27
- package/aeg-root/state-machine.md +9 -11
- package/aeg-root/templates/brief-template.md +6 -6
- package/aeg-root/templates/pr-report-template.md +10 -7
- package/aeg-root/tranche-model.md +6 -2
- package/dist/checks/bin/check-body-bare-digits.js +559 -140
- package/dist/checks/bin/check-branch-topology.js +630 -145
- package/dist/checks/bin/check-brief-shape.js +600 -143
- package/dist/checks/bin/check-changeset-coverage.js +942 -162
- package/dist/checks/bin/check-closes-n.js +630 -145
- package/dist/checks/bin/check-coherence.js +641 -148
- package/dist/checks/bin/check-dead-branch-push.js +559 -140
- package/dist/checks/bin/check-dispatch-readiness.js +639 -146
- package/dist/checks/bin/check-doc-coverage-push.js +696 -219
- package/dist/checks/bin/check-doc-coverage.js +696 -219
- package/dist/checks/bin/check-doctrine-no-procedures.js +4800 -0
- package/dist/checks/bin/check-doctrine-portability.js +691 -147
- package/dist/checks/bin/check-evidence-fresh.js +1048 -222
- package/dist/checks/bin/check-exec-bits.js +4937 -0
- package/dist/checks/bin/check-first-push-dispatch.js +630 -145
- package/dist/checks/bin/check-issue-assignment.js +630 -145
- package/dist/checks/bin/check-main-branch-refusal.js +565 -141
- package/dist/checks/bin/check-no-disk-state.js +559 -140
- package/dist/checks/bin/check-pr-report-density.js +4467 -0
- package/dist/checks/bin/check-quoted-command.js +656 -164
- package/dist/checks/bin/check-reader-resolvable-prose.js +652 -163
- package/dist/checks/bin/check-registry-gates.js +602 -145
- package/dist/checks/bin/check-retired-vocabulary.js +652 -163
- package/dist/checks/bin/check-review-gate.js +677 -142
- package/dist/checks/bin/check-single-plan-pr.js +559 -140
- package/dist/checks/bin/check-test-plan.js +560 -141
- package/dist/checks/bin/check-token-collection-wired.js +559 -140
- package/dist/checks/bin/check-token-report.js +559 -140
- package/dist/checks/bin/check-workspace-escape.js +992 -146
- package/dist/index.js +8884 -5627
- package/package.json +1 -1
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/actions.ts +173 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/anchored-region.ts +118 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/archive-task.ts +206 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/baseline-capture.ts +65 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/blast-radius-domains.ts +198 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/branch-topology-gate.ts +85 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-render.ts +462 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-validation.ts +912 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/claude-code-transcript.ts +460 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/coherence-checks.ts +806 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/consumer-enumeration.ts +75 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-audit.ts +55 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-guard.ts +77 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-section7.ts +66 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-tranche.ts +94 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/diagram-model.ts +372 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/direct-main-push.ts +39 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dispatch-gate.ts +268 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doc-owners.ts +314 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/build-doc-nav.ts +38 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/docs-coherence.ts +136 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/index.ts +29 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/legacy-anchors.ts +48 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/nav-helpers.ts +17 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/node-route.ts +134 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/parse-doc.ts +43 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/published-prose.ts +306 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/surfaced-manifest.ts +89 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/types.ts +33 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-no-procedures.ts +94 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-portability.ts +293 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-source.ts +23 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/ensure-label.ts +28 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/file-classify.ts +51 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/first-push-dispatch-gate.ts +80 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/gate-audience.ts +171 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/index.ts +323 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-assignment.ts +102 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-validation.ts +746 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/leftover-detection.ts +68 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/local-anchor-coverage.ts +245 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/main-branch-refusal.ts +64 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/manifest-validity.ts +81 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/markdown-table.ts +76 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/metering-io-guard.ts +119 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/milestone-validation.ts +240 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/no-disk-state.ts +50 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-ledger.ts +77 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-registry.ts +105 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-token-report.ts +177 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-tranche.ts +183 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-report-density.ts +116 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-tier.ts +81 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/premise-check.ts +160 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/quoted-command.ts +275 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/reader-resolvable-prose.ts +320 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-checks.ts +255 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-parse.ts +164 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-scaffold.ts +216 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/report-tokens.ts +261 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/retired-vocabulary.ts +163 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-gate.ts +378 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-status.ts +200 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/single-plan-pr.ts +73 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-machine-model.ts +269 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-source.ts +14 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/status-block.ts +8 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/sum-ledger.ts +26 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/symbol-collisions.ts +78 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-gate.ts +117 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-section.ts +65 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/types.ts +174 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/verdict-extraction.ts +199 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/vocabulary-citation.ts +114 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/waiver-label.ts +52 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/workspace-escape.ts +172 -0
- 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/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/_head.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.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/api/coherence/route.js.nft.json +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.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.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/[root-of-the-server]__02a-3g8._.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]__0053k9k._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__069-_41._.js → [root-of-the-server]__1pndh3_._.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.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]_05ufo8i._.js +3 -3
- 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/{37hnluhzj9bei.js → 0zebsmmk1bvnb.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{267o-tsrnuwnv.js → 1uogpj5w2n5ju.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1hufpuody6vjv.js → 3m1kgax7j2vgs.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- /package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → QY2GhiI47-765D9DHGy1I}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → QY2GhiI47-765D9DHGy1I}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → QY2GhiI47-765D9DHGy1I}/_ssgManifest.js +0 -0
package/package.json
CHANGED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* actions.ts — the canonical, aeg-core-owned enumeration of every AEG action
|
|
3
|
+
* that either crosses into GitHub or hands work across a role-seam contract.
|
|
4
|
+
* Pure data, zero I/O, zero imports beyond the type itself — same discipline
|
|
5
|
+
* as `waiver-label.ts`.
|
|
6
|
+
*
|
|
7
|
+
* Two consumers read this one list (Issue #505): G3's "no seventh way into
|
|
8
|
+
* GitHub" completeness story (`registry-checks.ts`) targets the
|
|
9
|
+
* `crosses: 'into-github'` subset against `enforcement.md`'s Ring-0 gate rows;
|
|
10
|
+
* task 506's DiagramModel places each action both inside a ring-0 gate (as a
|
|
11
|
+
* guarded crossing) and on the edge between role/contract nodes (as a seam
|
|
12
|
+
* hand-off). One list, so an `ACTIONS.length`-driven diagram edge count and
|
|
13
|
+
* G3's crossing list can never drift apart.
|
|
14
|
+
*
|
|
15
|
+
* The set is 11 distinct actions: the 6 crossings named in Issue #505 plus 5
|
|
16
|
+
* seam-only actions (4 original, plus `create-the-milestone`,
|
|
17
|
+
* vinaya-milestone-model-v1 task 2). Two of the six contract seams are already
|
|
18
|
+
* accomplished by a GitHub crossing (the Planner→Brief seam's carrier is the
|
|
19
|
+
* Issue body, created by `create-a-task-issue`; the Developer→Reviewer seam's
|
|
20
|
+
* carrier is the PR, created by `open-a-pull-request`), so they get no
|
|
21
|
+
* duplicate id — inventing one would give two ids for one real act and break
|
|
22
|
+
* any `ACTIONS.length`-driven edge count.
|
|
23
|
+
*
|
|
24
|
+
* `crosses` is factually per-action, not a mirror of Issue #505's prose:
|
|
25
|
+
* `commit-the-work` is `'none'` because `git commit` never leaves the local
|
|
26
|
+
* machine — it is `git push` (`publish-the-branch`) that reaches the
|
|
27
|
+
* GitHub-hosted remote. `enforcement.md`'s own Ring-0 table lists them as
|
|
28
|
+
* separate rows (`.husky/pre-commit` vs `.husky/pre-push`) for exactly this
|
|
29
|
+
* reason..
|
|
30
|
+
*
|
|
31
|
+
* The only valid `performedBy` values are the 9 `role_id`s under
|
|
32
|
+
* `aeg-root/roles/*.md` (task 2, #522) — asserted by the real-file test.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export type ActionCrossing = 'into-github' | 'none'
|
|
36
|
+
|
|
37
|
+
export type Action = {
|
|
38
|
+
id: string
|
|
39
|
+
label: string
|
|
40
|
+
crosses: ActionCrossing
|
|
41
|
+
performedBy: string[] // role_id[]
|
|
42
|
+
/** The rhetorical question — what a reader recognises. */
|
|
43
|
+
summary: string
|
|
44
|
+
/** What the action actually is, plainly. The counterpart to `summary`:
|
|
45
|
+
* the question earns attention, this answers it. Required, so a new action
|
|
46
|
+
* cannot be added without one — the same bar `roles/*.md` and
|
|
47
|
+
* `contracts/*.md` hold via their `description:` frontmatter. */
|
|
48
|
+
description: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const ACTIONS: Action[] = [
|
|
52
|
+
{
|
|
53
|
+
id: 'publish-the-branch',
|
|
54
|
+
label: 'publish the branch',
|
|
55
|
+
crosses: 'into-github',
|
|
56
|
+
performedBy: ['developer'],
|
|
57
|
+
summary: 'Ever had a branch pushed straight to main by mistake?',
|
|
58
|
+
description: 'Pushing local commits up to GitHub, where the rest of the mechanism can finally see them.'
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'create-a-task-issue',
|
|
62
|
+
label: 'create a task issue',
|
|
63
|
+
crosses: 'into-github',
|
|
64
|
+
performedBy: ['planner'],
|
|
65
|
+
summary: 'Ever opened a ticket that turned out to have zero plan behind it?',
|
|
66
|
+
description:
|
|
67
|
+
'Opening the Issue that a task exists as — its scope, its reasoning and its dependencies, written down before anyone starts.'
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'open-a-pull-request',
|
|
71
|
+
label: 'open a pull request',
|
|
72
|
+
crosses: 'into-github',
|
|
73
|
+
performedBy: ['developer'],
|
|
74
|
+
summary: 'Ever gotten a PR with no explanation of what it does or why?',
|
|
75
|
+
description:
|
|
76
|
+
'Proposing finished work for review, carrying the account of what changed and which intent it came from.'
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'revise-a-pull-request',
|
|
80
|
+
label: 'revise a pull request',
|
|
81
|
+
crosses: 'into-github',
|
|
82
|
+
performedBy: ['developer', 'planner'],
|
|
83
|
+
summary: 'Ever seen a PR quietly gutted after it already passed review?',
|
|
84
|
+
description:
|
|
85
|
+
'Editing a pull request after it exists — its code, its title or its description, whether or not review already happened.'
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'grant-a-waiver',
|
|
89
|
+
label: 'grant a waiver',
|
|
90
|
+
crosses: 'into-github',
|
|
91
|
+
performedBy: ['principal'],
|
|
92
|
+
summary: 'Ever had someone bypass a rule just by typing the right words?',
|
|
93
|
+
description:
|
|
94
|
+
'Deliberately excusing a rule for one case — an authority the mechanism grants to a person, never to an agent.'
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'commit-the-work',
|
|
98
|
+
label: 'commit the work',
|
|
99
|
+
crosses: 'none',
|
|
100
|
+
performedBy: ['developer'],
|
|
101
|
+
summary: "Ever had a commit land that plain doesn't build?",
|
|
102
|
+
description: 'Recording a change locally — the last moment it costs nothing to catch a mistake.'
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'author-the-brief',
|
|
106
|
+
label: 'author the brief',
|
|
107
|
+
crosses: 'none',
|
|
108
|
+
performedBy: ['brief-author'],
|
|
109
|
+
summary: 'Ever started a task with no clear idea of what "done" means?',
|
|
110
|
+
description:
|
|
111
|
+
'Turning one intent into instructions someone can execute: what to build, what is out of scope, and what done means.'
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'produce-the-verdict',
|
|
115
|
+
label: 'produce the verdict',
|
|
116
|
+
crosses: 'none',
|
|
117
|
+
performedBy: ['reviewer', 'security'],
|
|
118
|
+
summary: 'Ever gotten a review that was really just a rubber stamp?',
|
|
119
|
+
description: 'Judging finished work against the brief it came from, and saying plainly whether it passes.'
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: 'post-provenance-comment',
|
|
123
|
+
label: 'post the provenance comment',
|
|
124
|
+
crosses: 'none',
|
|
125
|
+
performedBy: ['archivist'],
|
|
126
|
+
summary: 'Ever needed to know exactly what shipped, and why, six months later?',
|
|
127
|
+
description: 'Writing the permanent record of a merged task — what shipped, from what intent, checked by whom.'
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'write-the-retrospective',
|
|
131
|
+
label: 'write the retrospective',
|
|
132
|
+
crosses: 'none',
|
|
133
|
+
performedBy: ['tranche-archivist'],
|
|
134
|
+
summary: 'Ever finished a big chunk of work with nobody writing down what was learned?',
|
|
135
|
+
description: 'Closing out a finished phase of work by recording what actually happened and what it taught.'
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: 'create-the-milestone',
|
|
139
|
+
label: 'create the milestone',
|
|
140
|
+
// Not into-github: `crosses` marks the specific Ring-0 local-git-hook
|
|
141
|
+
// crossing class G3 polices (raw `gh issue create`/`gh pr create`/`git
|
|
142
|
+
// push` bypass), not "touches the GitHub API" generally — the same
|
|
143
|
+
// reason `post-provenance-comment`/`produce-the-verdict` are 'none'
|
|
144
|
+
// despite writing PR comments via the API. `vinaya milestone create`
|
|
145
|
+
// is a manually-invoked CLI command with no Ring-0 hook intercepting a
|
|
146
|
+
// raw bypass, so it stays seam-only here (vinaya-milestone-model-v1
|
|
147
|
+
// task 2).
|
|
148
|
+
crosses: 'none',
|
|
149
|
+
performedBy: ['architect'],
|
|
150
|
+
summary: 'Ever had a Milestone whose title quietly became the only place a version lived?',
|
|
151
|
+
description:
|
|
152
|
+
"Declaring a product goal as a Milestone — free-text title, prose goal, an optional Release: field as the version's sole authority, and an optional list of which tranches serve it."
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
// A stable keyword per into-github action that must appear (case-insensitive)
|
|
157
|
+
// in some real Ring-0 gate row's action text — the "feeds G3" tripwire from
|
|
158
|
+
// Issue #505. If enforcement.md ever drops the gate row backing a crossing,
|
|
159
|
+
// the corresponding assertion fails. grant-a-waiver's keyword is
|
|
160
|
+
// `pull request`: granting a waiver is an act on a pull request (labeling it),
|
|
161
|
+
// honored at the Ring-0 PR-creation/editing gate and refused-from-agents at
|
|
162
|
+
// the raw-API gate — both reference pull requests. See PR body.
|
|
163
|
+
//
|
|
164
|
+
// Consumed by both `actions.test.ts` (the crossing tripwire) and
|
|
165
|
+
// `diagram-model.ts` (the `guards` gate→action edges), so the one keyword map
|
|
166
|
+
// can never drift between them (same no-drift principle as ACTIONS).
|
|
167
|
+
export const CROSSING_KEYWORDS: Record<string, string> = {
|
|
168
|
+
'publish-the-branch': 'git push',
|
|
169
|
+
'create-a-task-issue': 'task issue',
|
|
170
|
+
'open-a-pull-request': 'pull request',
|
|
171
|
+
'revise-a-pull-request': 'pull request',
|
|
172
|
+
'grant-a-waiver': 'pull request'
|
|
173
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AEG anchored-region grammar (aeg-governance-hardening task 30, #393). Pure —
|
|
3
|
+
* no `fs`, no `gh`/`git`.
|
|
4
|
+
*
|
|
5
|
+
* One anchor syntax for every gate-read field: an HTML comment pair
|
|
6
|
+
* `<!-- AEG:<FIELD>:START -->` … `<!-- AEG:<FIELD>:END -->` delimiting the
|
|
7
|
+
* field's one canonical home inside a PR/Issue body. HTML comments render
|
|
8
|
+
* invisibly on the forge, survive inside `<details>` blocks, and can never be
|
|
9
|
+
* produced accidentally by freeform prose — which is exactly the failure class
|
|
10
|
+
* this closes (PR #392's pasted reference brief duplicated its own Test Plan
|
|
11
|
+
* inside the PR body; #363/#377 were the same shape as real bugs).
|
|
12
|
+
*
|
|
13
|
+
* Recognition semantics, shared by every consumer
|
|
14
|
+
* (`pr-tier.ts`, `test-plan-section.ts`, `premise-check.ts`,
|
|
15
|
+
* `brief-validation.ts`/`archive-task.ts`'s Project read, and the `Closes #N`
|
|
16
|
+
* reads in `coherence-checks.ts`/`archive-task.ts`):
|
|
17
|
+
*
|
|
18
|
+
* - **Anchors are additive, never required.** A body with no anchor pair for
|
|
19
|
+
* a field parses byte-identically to the pre-anchor behavior — every
|
|
20
|
+
* already-merged PR/Issue keeps reading exactly as today.
|
|
21
|
+
* - **When a pair is present, it is authoritative.** The consumer reads the
|
|
22
|
+
* field exclusively from inside the pair and ignores identical-looking
|
|
23
|
+
* text anywhere else in the body — there is deliberately no fallback to
|
|
24
|
+
* body-wide search, since falling back would resurrect the decoy problem
|
|
25
|
+
* the anchor exists to solve.
|
|
26
|
+
* - **First pair wins** when a field is (incorrectly) anchored twice —
|
|
27
|
+
* mirroring the first-match-wins philosophy of the existing prose parsers.
|
|
28
|
+
* - **A `START` with no following `END` is treated as no anchor at all**
|
|
29
|
+
* (prose fallback) — a malformed half-pair must not be able to hide a
|
|
30
|
+
* field from the gates.
|
|
31
|
+
* - **Markers inside fenced code blocks or inline code spans do not count**
|
|
32
|
+
* — example/quoted anchor syntax in documentation or evidence output is
|
|
33
|
+
* never mistaken for a real anchor (same code-stripping philosophy as
|
|
34
|
+
* `archive-task.ts`'s `stripCode`, #311 regression).
|
|
35
|
+
*
|
|
36
|
+
* The field line/block goes on its own line(s) INSIDE the pair — e.g.
|
|
37
|
+
*
|
|
38
|
+
* <!-- AEG:TIER:START -->
|
|
39
|
+
* **Tier:** 1
|
|
40
|
+
* <!-- AEG:TIER:END -->
|
|
41
|
+
*
|
|
42
|
+
* Deliberately minimal: seven field names, one shape. This is a delimiting
|
|
43
|
+
* convention, not a metadata DSL; resist adding structure to it.
|
|
44
|
+
*
|
|
45
|
+
* `EVIDENCE` (fix/pr-report-emitter) differs from the other six in one way:
|
|
46
|
+
* it is never hand-typed. `vinaya pr report --write` is the only writer, and
|
|
47
|
+
* `check-evidence-fresh` is the only reader — no prose-fallback recognition
|
|
48
|
+
* exists or is planned for this field, unlike the anchor-optional grammar
|
|
49
|
+
* described above for the other five.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import { maskCode } from '@attalabs/aeg-forge-state/strip-code'
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The seven gate-read fields with an anchored home. `TOKENS` (task 5, #378)
|
|
56
|
+
* joined here rather than through a second, parallel field list —
|
|
57
|
+
* `pr-report.ts`'s own `AEG:TOKENS:START`/`:END` markers already use this
|
|
58
|
+
* exact grammar, so the registry is the fix, not a workaround beside it.
|
|
59
|
+
*/
|
|
60
|
+
export const ANCHOR_FIELDS = ['CLOSES', 'PROJECT', 'TIER', 'PREMISE', 'TEST-PLAN', 'EVIDENCE', 'TOKENS'] as const
|
|
61
|
+
|
|
62
|
+
export type AnchorField = (typeof ANCHOR_FIELDS)[number]
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The code-recognition grammar — `stripCode`, `maskCode` and their fence /
|
|
66
|
+
* indented-block / inline-span scanners — moved down to
|
|
67
|
+
* `@attalabs/aeg-forge-state` (`strip-code.ts`) so `projectFieldFromBody` can
|
|
68
|
+
* read a body "minus its examples" through the SAME implementation rather than
|
|
69
|
+
* a second copy of it. `aeg-core` already depends on `aeg-forge-state`, so the
|
|
70
|
+
* grammar could only be shared by moving it down; importing upward would close
|
|
71
|
+
* a cycle. It is re-exported here because `anchored-region` remains this
|
|
72
|
+
* package's documented home for body-parsing primitives and every existing
|
|
73
|
+
* `import { stripCode } from './anchored-region'` call site is unchanged.
|
|
74
|
+
*/
|
|
75
|
+
export { stripCode } from '@attalabs/aeg-forge-state'
|
|
76
|
+
export type { StripCodeOptions } from '@attalabs/aeg-forge-state'
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The START/END markers' positions in `body`, found the same code-blind way
|
|
80
|
+
* `anchoredRegion` finds its content — `null` under the identical conditions
|
|
81
|
+
* `anchoredRegion` returns `null`. `outerStart`/`outerEnd` span the markers
|
|
82
|
+
* themselves; `innerStart`/`innerEnd` span the content between them (what
|
|
83
|
+
* `anchoredRegion` returns as a string).
|
|
84
|
+
*
|
|
85
|
+
* Exists so a WRITER (`vinaya pr report --write`'s `replaceEvidenceBlock`)
|
|
86
|
+
* can target the same real, non-fenced pair a reader would — a naive
|
|
87
|
+
* `body.indexOf(START)` finds whichever copy comes first in raw text, fenced
|
|
88
|
+
* decoy included, and a body that quotes a worked example of its own anchor
|
|
89
|
+
* (exactly what this task's own PR body does, in its Test Plan evidence)
|
|
90
|
+
* would get ITS OWN replacement written into the quoted example instead of
|
|
91
|
+
* the real field. One masked search, shared by the reader and the writer,
|
|
92
|
+
* closes that by construction rather than by convention.
|
|
93
|
+
*/
|
|
94
|
+
export function anchoredRegionBounds(
|
|
95
|
+
body: string,
|
|
96
|
+
field: AnchorField
|
|
97
|
+
): { outerStart: number; outerEnd: number; innerStart: number; innerEnd: number } | null {
|
|
98
|
+
const masked = maskCode(body)
|
|
99
|
+
const start = new RegExp(`<!--\\s*AEG:${field}:START\\s*-->`).exec(masked)
|
|
100
|
+
if (!start) return null
|
|
101
|
+
const innerStart = start.index + start[0].length
|
|
102
|
+
const end = new RegExp(`<!--\\s*AEG:${field}:END\\s*-->`).exec(masked.slice(innerStart))
|
|
103
|
+
if (!end) return null
|
|
104
|
+
const innerEnd = innerStart + end.index
|
|
105
|
+
return { outerStart: start.index, outerEnd: innerEnd + end[0].length, innerStart, innerEnd }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The text between the first `<!-- AEG:<field>:START -->` and the first
|
|
110
|
+
* `<!-- AEG:<field>:END -->` after it, or `null` when the body carries no
|
|
111
|
+
* (well-formed, non-code) pair for this field. `null` is the signal for
|
|
112
|
+
* consumers to run their unchanged prose/heading recognition.
|
|
113
|
+
*/
|
|
114
|
+
export function anchoredRegion(body: string, field: AnchorField): string | null {
|
|
115
|
+
const bounds = anchoredRegionBounds(body, field)
|
|
116
|
+
if (!bounds) return null
|
|
117
|
+
return body.slice(bounds.innerStart, bounds.innerEnd)
|
|
118
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-merge Archivist provenance assembly (aeg-governance-hardening
|
|
3
|
+
* task 5d, #309). Pure — no `fs`, no `fetch`, no `process.env`. The CLI shim
|
|
4
|
+
* (`bin/archive-task.ts`) resolves the merged PR via `gh`, gathers
|
|
5
|
+
* `MergedPrFacts`, and calls these functions.
|
|
6
|
+
*
|
|
7
|
+
* The cardinal constraint (`aeg-root/roles/archivist.md`): ASSEMBLE, never
|
|
8
|
+
* author. Every provenance field is copied from a fact the merge already
|
|
9
|
+
* froze — the PR body, PR metadata, or PR comments. A field whose source
|
|
10
|
+
* fact is absent becomes a DANGLING entry, never an inferred or defaulted
|
|
11
|
+
* value. Field/heading shapes below mirror the live provenance comments
|
|
12
|
+
* posted by hand on PRs #302/#305/#306.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { hasLabel } from '@attalabs/aeg-forge-state'
|
|
16
|
+
import { type AnchorField, anchoredRegion, stripCode } from './anchored-region'
|
|
17
|
+
import { headerRegion } from './brief-validation'
|
|
18
|
+
import { readTierFromPrBody } from './pr-tier'
|
|
19
|
+
import { extractCodeReviewVerdict, extractSecurityReviewVerdict } from './verdict-extraction'
|
|
20
|
+
|
|
21
|
+
export type MergedPrFacts = {
|
|
22
|
+
number: number
|
|
23
|
+
headRefName: string
|
|
24
|
+
body: string
|
|
25
|
+
mergedAt: string
|
|
26
|
+
mergeSha: string
|
|
27
|
+
comments: string[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TASK_BRANCH_PATTERN = /^task\/([^/]+)\/([^/]+)$/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* null when headRefName is not task/<tranche>/<taskId>.
|
|
34
|
+
*
|
|
35
|
+
* This is ONE of two independent eligibility signals for provenance —
|
|
36
|
+
* the branch-name pattern — not the only one. A PR that closes a
|
|
37
|
+
* tranche-labeled Issue from a non-`task/*` branch (e.g. a small ad-hoc
|
|
38
|
+
* `fix/*` cleanup that finally attaches `Closes #N` to a task whose real
|
|
39
|
+
* work already shipped elsewhere) is EQUALLY eligible: the CLI shim
|
|
40
|
+
* (`bin/archive-task.ts`) additionally checks the closed Issue's own
|
|
41
|
+
* `vinaya/tranche:*` label via `gh issue view --json labels` and proceeds if
|
|
42
|
+
* either signal is present. `buildProvenanceBlock` already tolerates a
|
|
43
|
+
* `null` ref (falls back to a branch-name task label), so this function's
|
|
44
|
+
* only remaining job is the branch-name half of that OR — it is no longer
|
|
45
|
+
* the sole gate. (Confirmed gap: a task whose Issue carried a
|
|
46
|
+
* `vinaya/tranche:*` label was closed by a `fix/*`-branch PR;
|
|
47
|
+
* branch-name-only detection silently skipped provenance forever.)
|
|
48
|
+
*/
|
|
49
|
+
export function taskRefFromBranch(branch: string): { tranche: string; taskId: string } | null {
|
|
50
|
+
const m = branch.match(TASK_BRANCH_PATTERN)
|
|
51
|
+
if (!m) return null
|
|
52
|
+
return { tranche: m[1] as string, taskId: m[2] as string }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The eligibility decision itself, pulled out as a pure function so it's
|
|
57
|
+
* testable without mocking `gh` — the CLI shim (`bin/archive-task.ts`) does
|
|
58
|
+
* only the I/O (resolve `ref`, fetch the closed Issue's labels) and hands
|
|
59
|
+
* both facts here. True when EITHER signal holds: a real task-branch `ref`,
|
|
60
|
+
* or the closed Issue's own labels carry an `vinaya/tranche:*` tag.
|
|
61
|
+
*/
|
|
62
|
+
export function isEligibleForProvenance(
|
|
63
|
+
ref: { tranche: string; taskId: string } | null,
|
|
64
|
+
issueLabels: string[]
|
|
65
|
+
): boolean {
|
|
66
|
+
if (ref !== null) return true
|
|
67
|
+
return hasLabel('tranche', issueLabels)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const PROVENANCE_HEADING = '### AEG provenance'
|
|
71
|
+
|
|
72
|
+
/** true when any comment already carries the provenance heading (idempotency, scoped to this PR). */
|
|
73
|
+
export function hasProvenance(comments: string[]): boolean {
|
|
74
|
+
return comments.some((c) => c.includes(PROVENANCE_HEADING))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Tolerant field reader: accepts `Field:` and `**Field:**`, line-anchored,
|
|
79
|
+
* searched ONLY in the header region (shared with `brief-validation.ts`'s
|
|
80
|
+
* gate — gate and archivist read the same region, so a body that passes the
|
|
81
|
+
* gate can't produce a DANGLING field here). Whole-body scanning is the
|
|
82
|
+
* regression from #311's first live run, where a prose sentence *about* the
|
|
83
|
+
* `Ticket:` field in a later section was extracted as the field's value.
|
|
84
|
+
*
|
|
85
|
+
* When `anchor` is given and the body carries that anchor pair
|
|
86
|
+
* (`anchored-region.ts`, task 30), the pair replaces the header region as the
|
|
87
|
+
* one place the field is read from — same recognition the gate side
|
|
88
|
+
* (`checkProjectField`) uses, preserving gate/archivist parity.
|
|
89
|
+
*/
|
|
90
|
+
function extractField(body: string, labelPattern: string, anchor?: AnchorField): string | null {
|
|
91
|
+
const anchored = anchor !== undefined ? anchoredRegion(body, anchor) : null
|
|
92
|
+
const region = anchored ?? headerRegion(body)
|
|
93
|
+
const re = new RegExp(`^(?:\\*\\*)?\\s*${labelPattern}\\s*(?:\\*\\*)?\\s*:\\s*(?:\\*\\*)?\\s*([^\\n·]+)`, 'im')
|
|
94
|
+
const m = region.match(re)
|
|
95
|
+
if (!m) return null
|
|
96
|
+
const value = (m[1] as string).trim()
|
|
97
|
+
return value.length > 0 ? value : null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function closesRefs(text: string): number[] {
|
|
101
|
+
return [...new Set([...text.matchAll(/Closes #(\d+)/gi)].map((m) => Number(m[1] as string)))]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Primary Issue = first `Closes #N` in the header block (canonical placement),
|
|
106
|
+
* falling back to the first anywhere in the body (flagged, not lost). Extras
|
|
107
|
+
* are scanned body-wide so a real second closing reference in prose is still
|
|
108
|
+
* flagged — but always fence-stripped, so example text like a Test Plan's
|
|
109
|
+
* `Closes #123` fixture never counts (#311 regression).
|
|
110
|
+
*
|
|
111
|
+
* When the body carries an `AEG:CLOSES` anchor pair (`anchored-region.ts`,
|
|
112
|
+
* task 30), the pair replaces the header block as the canonical placement:
|
|
113
|
+
* the primary Issue is the first `Closes #N` inside the pair (never flagged
|
|
114
|
+
* `outsideHeader` — anchored IS canonical), and extras are still scanned
|
|
115
|
+
* body-wide so stray closing references stay flagged. Bodies without the
|
|
116
|
+
* pair parse exactly as before.
|
|
117
|
+
*/
|
|
118
|
+
export function extractIssue(body: string): { issue: number | null; extraIssues: number[]; outsideHeader: boolean } {
|
|
119
|
+
// Strip ONCE over the whole body, then slice — never strip a slice. The
|
|
120
|
+
// strip's block rules read context a fragment no longer carries: an anchor
|
|
121
|
+
// indented inside a list item is list content in the full body (kept, and
|
|
122
|
+
// GitHub *does* auto-close it) but looks like a bare 4-space indented code
|
|
123
|
+
// block once sliced, so stripping the region blanked the reference and
|
|
124
|
+
// returned `issue: null` — the Issue then goes unclosed on merge, the exact
|
|
125
|
+
// stranding this PR exists to eliminate, reintroduced along the over-strip
|
|
126
|
+
// axis (PR #617 review MAJOR). Stripping first also subsumes the decoy
|
|
127
|
+
// protection rather than trading against it: `AEG:*` markers are HTML
|
|
128
|
+
// comments and survive the strip, while a decoy anchor inside code does not
|
|
129
|
+
// survive to be sliced in the first place.
|
|
130
|
+
const stripped = stripCode(body)
|
|
131
|
+
const bodyNums = closesRefs(stripped)
|
|
132
|
+
const anchored = anchoredRegion(stripped, 'CLOSES')
|
|
133
|
+
if (anchored !== null) {
|
|
134
|
+
const anchorNums = closesRefs(anchored)
|
|
135
|
+
if (anchorNums.length === 0) return { issue: null, extraIssues: bodyNums, outsideHeader: false }
|
|
136
|
+
const issue = anchorNums[0] as number
|
|
137
|
+
return { issue, extraIssues: bodyNums.filter((n) => n !== issue), outsideHeader: false }
|
|
138
|
+
}
|
|
139
|
+
const headerNums = closesRefs(headerRegion(stripped))
|
|
140
|
+
if (bodyNums.length === 0) return { issue: null, extraIssues: [], outsideHeader: false }
|
|
141
|
+
const issue = headerNums.length > 0 ? (headerNums[0] as number) : (bodyNums[0] as number)
|
|
142
|
+
return { issue, extraIssues: bodyNums.filter((n) => n !== issue), outsideHeader: headerNums.length === 0 }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Assemble the block purely from frozen facts; absent facts become DANGLING entries. */
|
|
146
|
+
export function buildProvenanceBlock(facts: MergedPrFacts): {
|
|
147
|
+
block: string
|
|
148
|
+
issue: number | null
|
|
149
|
+
dangling: string[]
|
|
150
|
+
} {
|
|
151
|
+
const ref = taskRefFromBranch(facts.headRefName)
|
|
152
|
+
const dangling: string[] = []
|
|
153
|
+
|
|
154
|
+
const { issue, extraIssues, outsideHeader } = extractIssue(facts.body)
|
|
155
|
+
if (issue === null) {
|
|
156
|
+
dangling.push('no `Closes #N` found in PR body — Issue field is DANGLING, no Issue will be closed')
|
|
157
|
+
}
|
|
158
|
+
if (outsideHeader) {
|
|
159
|
+
dangling.push(
|
|
160
|
+
`\`Closes #${issue}\` was found outside the PR body's header block — the canonical form places it before the first \`##\` heading`
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
if (extraIssues.length > 0) {
|
|
164
|
+
dangling.push(
|
|
165
|
+
`PR body references additional Issues (${extraIssues.map((n) => `#${n}`).join(', ')}) beyond the first — closing only #${issue}, the rest are flagged, not closed`
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const tier = readTierFromPrBody(facts.body)
|
|
170
|
+
if (tier === null) dangling.push('no `Tier:` field found in PR body — Tier field is DANGLING')
|
|
171
|
+
|
|
172
|
+
const project = extractField(facts.body, 'Project(?:\\(s\\))?', 'PROJECT')
|
|
173
|
+
if (!project) dangling.push('no `Project:` field found in PR body — Project(s) field is DANGLING')
|
|
174
|
+
|
|
175
|
+
const forField = extractField(facts.body, 'For')
|
|
176
|
+
if (!forField) dangling.push('no `For:` field found in PR body — Model/agent field is DANGLING')
|
|
177
|
+
|
|
178
|
+
const codeReview = extractCodeReviewVerdict(facts.comments)
|
|
179
|
+
if (codeReview.danglingNote) dangling.push(codeReview.danglingNote)
|
|
180
|
+
|
|
181
|
+
const security = extractSecurityReviewVerdict(facts.comments)
|
|
182
|
+
if (security.danglingNote) dangling.push(security.danglingNote)
|
|
183
|
+
|
|
184
|
+
const ticket = extractField(facts.body, 'Ticket') ?? 'none'
|
|
185
|
+
|
|
186
|
+
const taskLabel = ref ? `task ${ref.taskId} (tranche ${ref.tranche})` : `task (branch ${facts.headRefName})`
|
|
187
|
+
|
|
188
|
+
const lines = [
|
|
189
|
+
`${PROVENANCE_HEADING} — ${taskLabel}`,
|
|
190
|
+
`- Issue: ${issue !== null ? `#${issue} (closed by merge)` : 'DANGLING — no Closes #N in PR body'}`,
|
|
191
|
+
`- Tier: ${tier !== null ? tier : 'DANGLING — no Tier field in PR body'}`,
|
|
192
|
+
'- Brief: in this PR body (the frozen intent)',
|
|
193
|
+
`- Project(s): ${project ?? 'DANGLING — no Project field in PR body'}`,
|
|
194
|
+
`- Model/agent: ${forField ?? 'DANGLING — no For field in PR body'}`,
|
|
195
|
+
`- Code review: ${codeReview.value}`,
|
|
196
|
+
`- Security: ${security.value}`,
|
|
197
|
+
`- Ticket: ${ticket}`,
|
|
198
|
+
`- Merged: ${facts.mergeSha} at ${facts.mergedAt}`
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
if (dangling.length > 0) {
|
|
202
|
+
lines.push('', `DANGLING: ${dangling.join('; ')}`)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { block: lines.join('\n'), issue, dangling }
|
|
206
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding-count baseline capture + comparison (aeg-governance-hardening task
|
|
3
|
+
* 11, #324). Pure — does not run `verify-docs.ts`/`verify-coherence.ts`
|
|
4
|
+
* itself and never calls `Date.now()` (a pure `src/` module cannot — see
|
|
5
|
+
* `packages/aeg-core/CLAUDE.md`-equivalent constraint, this repo's is the
|
|
6
|
+
* Workflow-script rule of the same shape). The CLI shim runs those tools,
|
|
7
|
+
* counts their findings, and passes the counts plus a caller-supplied
|
|
8
|
+
* timestamp in here.
|
|
9
|
+
*
|
|
10
|
+
* Exists to make the dispatch-readiness bar honestly "no new findings versus
|
|
11
|
+
* the captured baseline" rather than "zero findings" — live-fire #2
|
|
12
|
+
* (aeg-governance-hardening's own dispatch wave) asserted "verify-docs full
|
|
13
|
+
* mode must be green" as a pre-flight precondition without ever running it;
|
|
14
|
+
* full mode had 44 pre-existing, unrelated findings and was never green.
|
|
15
|
+
* Baseline data is capture-at-run only, never a committed file.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type BaselineEntry = {
|
|
19
|
+
tool: string
|
|
20
|
+
findingCount: number
|
|
21
|
+
/** ISO timestamp supplied by the caller — never generated inside this module. */
|
|
22
|
+
capturedAt: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Stamps a set of tool→findingCount pairs into baseline records, using the caller-supplied `capturedAt`. */
|
|
26
|
+
export function captureBaseline(
|
|
27
|
+
counts: Array<{ tool: string; findingCount: number }>,
|
|
28
|
+
capturedAt: string
|
|
29
|
+
): BaselineEntry[] {
|
|
30
|
+
return counts.map((c) => ({ tool: c.tool, findingCount: c.findingCount, capturedAt }))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type BaselineToolComparison = { tool: string; baseline: number; current: number; delta: number }
|
|
34
|
+
|
|
35
|
+
export type BaselineComparison = {
|
|
36
|
+
/** True iff no tool's current finding count exceeds its baseline. */
|
|
37
|
+
withinBudget: boolean
|
|
38
|
+
/** Sum of every tool's delta (positive = net new findings across all tools). */
|
|
39
|
+
delta: number
|
|
40
|
+
perTool: BaselineToolComparison[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Compare current finding counts against a captured baseline, per tool.
|
|
45
|
+
* A tool absent from the baseline (never captured before) has nothing to
|
|
46
|
+
* regress against — its delta reports as `0` and its current count becomes
|
|
47
|
+
* the effective baseline going forward, rather than failing budget purely
|
|
48
|
+
* because no prior baseline was ever recorded for it.
|
|
49
|
+
*/
|
|
50
|
+
export function compareToBaseline(
|
|
51
|
+
current: Array<{ tool: string; findingCount: number }>,
|
|
52
|
+
baseline: BaselineEntry[]
|
|
53
|
+
): BaselineComparison {
|
|
54
|
+
const perTool: BaselineToolComparison[] = current.map((c) => {
|
|
55
|
+
const baselineEntry = baseline.find((b) => b.tool === c.tool)
|
|
56
|
+
const baselineCount = baselineEntry ? baselineEntry.findingCount : c.findingCount
|
|
57
|
+
return { tool: c.tool, baseline: baselineCount, current: c.findingCount, delta: c.findingCount - baselineCount }
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
withinBudget: perTool.every((t) => t.delta <= 0),
|
|
62
|
+
delta: perTool.reduce((sum, t) => sum + t.delta, 0),
|
|
63
|
+
perTool
|
|
64
|
+
}
|
|
65
|
+
}
|