@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
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* registry-checks.ts — G1–G6, the deterministic coherence checks that make
|
|
3
|
+
* `aeg-root/enforcement.md`'s three ring tables (parsed by
|
|
4
|
+
* `registry-parse.ts`) load-bearing instead of decorative.
|
|
5
|
+
*
|
|
6
|
+
* Pure — no `fs`, no `git`/`gh` I/O, no `fetch`. All forge/filesystem facts
|
|
7
|
+
* are injected by the caller (`bin/verify-registry.ts` /
|
|
8
|
+
* `apps/cli/src/checks/bin/check-registry-gates.ts`, the I/O shims),
|
|
9
|
+
* mirroring `coherence-checks.ts`'s shape exactly (plain executables,
|
|
10
|
+
* deterministic pass/fail, no config conditionals).
|
|
11
|
+
*
|
|
12
|
+
* Rollout policy: G2 ships report-only — it can only ever report `'info'`,
|
|
13
|
+
* never `'fail'`, so it never affects CI's exit code (the accepted shape for
|
|
14
|
+
* G2's inverse sweep, unchanged by task 8). G1 was report-only through its
|
|
15
|
+
* own rollout window; task 8 flips it to blocking now that the orphan
|
|
16
|
+
* backlog it existed to surface is clean (0 findings against the real
|
|
17
|
+
* `enforcement.md` at the time of the flip). G3–G6 are blocking (`'fail'` on
|
|
18
|
+
* any violation).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { GATE_AUDIENCE, isShipped } from './gate-audience'
|
|
22
|
+
import type { GateRow } from './registry-parse'
|
|
23
|
+
|
|
24
|
+
export type RegistryCheckStatus = 'pass' | 'fail' | 'info'
|
|
25
|
+
|
|
26
|
+
export type RegistryFinding = {
|
|
27
|
+
row?: string
|
|
28
|
+
path?: string
|
|
29
|
+
reason: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type RegistryCheckResult = {
|
|
33
|
+
check: 'G1' | 'G2' | 'G3' | 'G4' | 'G5' | 'G6'
|
|
34
|
+
status: RegistryCheckStatus
|
|
35
|
+
findings: RegistryFinding[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* G1 — every row's non-empty `implementation` resolves on disk. Blocking as
|
|
40
|
+
* of task 8 (re-graded from report-only: eleven permanent `info` findings on
|
|
41
|
+
* every run had become indistinguishable from silence, which is how the gap
|
|
42
|
+
* this tranche closes stayed invisible; the report-only window had already
|
|
43
|
+
* cleared the backlog it existed to surface).
|
|
44
|
+
*/
|
|
45
|
+
export function checkG1(rows: GateRow[], existsFn: (path: string) => boolean): RegistryCheckResult {
|
|
46
|
+
const findings: RegistryFinding[] = []
|
|
47
|
+
for (const row of rows) {
|
|
48
|
+
if (row.implementation === '') continue
|
|
49
|
+
if (!existsFn(row.implementation)) {
|
|
50
|
+
findings.push({
|
|
51
|
+
row: row.action,
|
|
52
|
+
path: row.implementation,
|
|
53
|
+
reason: `${row.ring} row "${row.action}" names implementation "${row.implementation}", which does not exist on disk`
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { check: 'G1', status: findings.length > 0 ? 'fail' : 'pass', findings }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The literal placeholder marker a registry-scaffold stub row's
|
|
61
|
+
* non-mechanical cells carry (`registry-scaffold.ts`'s `PLACEHOLDER`,
|
|
62
|
+
* duplicated here rather than imported to keep this module's zero-import
|
|
63
|
+
* shape — the two are asserted equal by `registry-checks.test.ts`). A stub
|
|
64
|
+
* that filled `implementation` alone would silence G2 outright (the whole
|
|
65
|
+
* defect Issue #104 exists to close): this marker is what keeps a row G2
|
|
66
|
+
* still flags loud until a human replaces it with the real "why". */
|
|
67
|
+
const SCAFFOLD_PLACEHOLDER = '[undocumented — fill in why]'
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* G2 — every candidate hook/CLI file is named by SOME row's `implementation`,
|
|
71
|
+
* AND no row still carries the scaffold's placeholder marker un-replaced.
|
|
72
|
+
* `candidateFiles` is already-globbed by the caller (`.husky/*`,
|
|
73
|
+
* `.claude/hooks/*.sh`, `packages/aeg-core/bin/*.ts`, excluding `.husky/_`).
|
|
74
|
+
* Report-only this tranche — same as G1, never `'fail'`.
|
|
75
|
+
*
|
|
76
|
+
* The second half (placeholder scan) is what keeps a registry-scaffold stub
|
|
77
|
+
* row visible: `implementation` presence alone would make a stub read as
|
|
78
|
+
* "documented" to the ORPHAN half above, which is precisely the trap the
|
|
79
|
+
* scaffold must not fall into. Scanned over `summary`/`description`/`spec`
|
|
80
|
+
* — every cell a stub can carry the marker in — never `implementation`,
|
|
81
|
+
* `action`, `category`, or `audience`, which the scaffold fills mechanically
|
|
82
|
+
* and are never expected to carry it.
|
|
83
|
+
*/
|
|
84
|
+
export function checkG2(rows: GateRow[], candidateFiles: string[]): RegistryCheckResult {
|
|
85
|
+
const implementations = new Set(rows.map((r) => r.implementation).filter((p) => p !== ''))
|
|
86
|
+
const findings: RegistryFinding[] = []
|
|
87
|
+
for (const path of candidateFiles) {
|
|
88
|
+
if (!implementations.has(path)) {
|
|
89
|
+
findings.push({
|
|
90
|
+
path,
|
|
91
|
+
reason: `"${path}" is not named as the implementation of any row in enforcement.md's ring tables`
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const row of rows) {
|
|
96
|
+
const carriesPlaceholder = [row.summary, row.description, row.spec].some((cell) => cell === SCAFFOLD_PLACEHOLDER)
|
|
97
|
+
if (carriesPlaceholder) {
|
|
98
|
+
findings.push({
|
|
99
|
+
row: row.action,
|
|
100
|
+
path: row.implementation,
|
|
101
|
+
reason: `${row.ring} row "${row.action}" still carries the scaffold placeholder marker — the why is owed`
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { check: 'G2', status: findings.length > 0 ? 'info' : 'pass', findings }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* G3 — every file that makes a GitHub-crossing call is named by SOME Ring-0
|
|
110
|
+
* row's `implementation` — "no seventh way into GitHub". `crossingFiles` is
|
|
111
|
+
* already-detected by the caller via a source-grep. Blocking.
|
|
112
|
+
*/
|
|
113
|
+
export function checkG3(ring0Rows: GateRow[], crossingFiles: string[]): RegistryCheckResult {
|
|
114
|
+
const ring0Implementations = new Set(ring0Rows.map((r) => r.implementation))
|
|
115
|
+
const findings: RegistryFinding[] = []
|
|
116
|
+
for (const path of crossingFiles) {
|
|
117
|
+
if (!ring0Implementations.has(path)) {
|
|
118
|
+
findings.push({
|
|
119
|
+
path,
|
|
120
|
+
reason: `"${path}" makes a GitHub-crossing call but is not named by any Ring-0 row's implementation — a seventh, unlisted way into GitHub`
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { check: 'G3', status: findings.length > 0 ? 'fail' : 'pass', findings }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* G4 — every `#NNN` cited in enforcement.md's body resolves in the forge.
|
|
129
|
+
* `resolveFn` wraps `gh issue view`/`gh pr view` (caller injects). Blocking.
|
|
130
|
+
* Broad reading (brief): every `#`-prefixed 3-or-more-digit number
|
|
131
|
+
* occurring anywhere in the body is checked, not just ones near keywords
|
|
132
|
+
* like "incident" — an incomplete keyword net would create a
|
|
133
|
+
* false-negative gap. The current repo's real citations are all 3-4
|
|
134
|
+
* digits; the lower bound of 3 exists only to exclude unrelated short
|
|
135
|
+
* numerals (ring/tier numbers) that are never written with a `#` prefix
|
|
136
|
+
* anyway — there is deliberately no upper bound, so a longer fabricated
|
|
137
|
+
* number is still caught.
|
|
138
|
+
*/
|
|
139
|
+
export function checkG4(content: string, resolveFn: (n: number) => boolean): RegistryCheckResult {
|
|
140
|
+
const cited = new Set<number>()
|
|
141
|
+
for (const hit of content.matchAll(/#(\d{3,})\b/g)) {
|
|
142
|
+
cited.add(Number(hit[1]))
|
|
143
|
+
}
|
|
144
|
+
const findings: RegistryFinding[] = []
|
|
145
|
+
for (const n of cited) {
|
|
146
|
+
if (!resolveFn(n)) {
|
|
147
|
+
findings.push({
|
|
148
|
+
reason: `#${n} is cited in enforcement.md but does not resolve to a real Issue or PR in the forge`
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { check: 'G4', status: findings.length > 0 ? 'fail' : 'pass', findings }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* G5 — every contract's producer/consumer is a real role_id; every role's
|
|
157
|
+
* `performs`/`refuses_when` is present and non-empty. Blocking.
|
|
158
|
+
*
|
|
159
|
+
* "The action exists" is satisfied by presence/well-formedness of the
|
|
160
|
+
* role's own frontmatter — there is no second, independent registry of
|
|
161
|
+
* valid actions to cross-reference `performs` entries against.
|
|
162
|
+
*/
|
|
163
|
+
export function checkG5(
|
|
164
|
+
roles: Array<{ file: string; role_id: string; performs: string[]; refuses_when: string }>,
|
|
165
|
+
contracts: Array<{ file: string; producer: string; consumer: string }>
|
|
166
|
+
): RegistryCheckResult {
|
|
167
|
+
const roleIds = new Set(roles.map((r) => r.role_id))
|
|
168
|
+
const findings: RegistryFinding[] = []
|
|
169
|
+
|
|
170
|
+
for (const contract of contracts) {
|
|
171
|
+
if (!roleIds.has(contract.producer)) {
|
|
172
|
+
findings.push({
|
|
173
|
+
path: contract.file,
|
|
174
|
+
reason: `contract "${contract.file}" names producer "${contract.producer}", which is not a real role_id`
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
if (!roleIds.has(contract.consumer)) {
|
|
178
|
+
findings.push({
|
|
179
|
+
path: contract.file,
|
|
180
|
+
reason: `contract "${contract.file}" names consumer "${contract.consumer}", which is not a real role_id`
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const role of roles) {
|
|
186
|
+
if (role.performs.length === 0) {
|
|
187
|
+
findings.push({ path: role.file, reason: `role "${role.role_id}" has an empty performs array` })
|
|
188
|
+
}
|
|
189
|
+
if (role.refuses_when.trim() === '') {
|
|
190
|
+
findings.push({ path: role.file, reason: `role "${role.role_id}" has an empty refuses_when` })
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { check: 'G5', status: findings.length > 0 ? 'fail' : 'pass', findings }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const SHIPPED_BIN_PATH_PREFIX = 'apps/cli/src/checks/bin/check-'
|
|
198
|
+
const AEG_CORE_BIN_PATH_PREFIX = 'packages/aeg-core/bin/'
|
|
199
|
+
|
|
200
|
+
function basenameNoExt(path: string): string {
|
|
201
|
+
const base = path.slice(path.lastIndexOf('/') + 1)
|
|
202
|
+
return base.replace(/\.ts$/, '')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Every `coreCheckRegistry()` name a `product`-audience row's implementation
|
|
207
|
+
* could resolve to. Two shapes: a path directly under the shipped bin
|
|
208
|
+
* directory names its own check 1:1 (`apps/cli/src/checks/bin/check-doc-
|
|
209
|
+
* coverage-push.ts` -> `doc-coverage-push`); a path under
|
|
210
|
+
* `packages/aeg-core/bin/` resolves through `GATE_AUDIENCE`, the same map
|
|
211
|
+
* `shipped-bin-audience.test.ts` (`apps/cli`) already asserts agrees with
|
|
212
|
+
* the registry — reused here rather than re-derived, so doctrine and code
|
|
213
|
+
* cannot silently disagree about what a bin ships as. Neither shape matches
|
|
214
|
+
* (e.g. a `.github/workflows/*.yml` path, `checks/runner.ts`, a forge-write
|
|
215
|
+
* command) resolves to no candidate at all — correct, since those are not
|
|
216
|
+
* `coreCheckRegistry()` checks by construction.
|
|
217
|
+
*/
|
|
218
|
+
function claimedCheckNames(implementation: string): string[] {
|
|
219
|
+
if (implementation.startsWith(SHIPPED_BIN_PATH_PREFIX) && implementation.endsWith('.ts')) {
|
|
220
|
+
return [implementation.slice(SHIPPED_BIN_PATH_PREFIX.length, -'.ts'.length)]
|
|
221
|
+
}
|
|
222
|
+
if (implementation.startsWith(AEG_CORE_BIN_PATH_PREFIX)) {
|
|
223
|
+
const audience = GATE_AUDIENCE[basenameNoExt(implementation)]
|
|
224
|
+
if (audience && isShipped(audience)) {
|
|
225
|
+
return Array.isArray(audience.shippedAs) ? audience.shippedAs : [audience.shippedAs]
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return []
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* G6 — every row doctrine marks `product` must actually resolve to a
|
|
233
|
+
* `coreCheckRegistry()` entry ("claimed checks must ship" — the parity gate
|
|
234
|
+
* this task exists for). `registeredCheckNames` is caller-injected
|
|
235
|
+
* (`coreCheckRegistry().map(s => s.name)`, from `apps/cli`, which
|
|
236
|
+
* `aeg-core` cannot import without closing a dependency cycle — same
|
|
237
|
+
* reasoning `gate-audience.ts` documents for `GATE_AUDIENCE` itself).
|
|
238
|
+
* Blocking. A row left at the default `repo-own` audience makes no shipped
|
|
239
|
+
* claim and is never checked here.
|
|
240
|
+
*/
|
|
241
|
+
export function checkG6(rows: GateRow[], registeredCheckNames: Set<string>): RegistryCheckResult {
|
|
242
|
+
const findings: RegistryFinding[] = []
|
|
243
|
+
for (const row of rows) {
|
|
244
|
+
if (row.audience !== 'product') continue
|
|
245
|
+
const candidates = claimedCheckNames(row.implementation)
|
|
246
|
+
if (!candidates.some((name) => registeredCheckNames.has(name))) {
|
|
247
|
+
findings.push({
|
|
248
|
+
row: row.action,
|
|
249
|
+
path: row.implementation,
|
|
250
|
+
reason: `${row.ring} row "${row.action}" is marked \`product\` but its implementation "${row.implementation}" does not resolve to a coreCheckRegistry() entry`
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return { check: 'G6', status: findings.length > 0 ? 'fail' : 'pass', findings }
|
|
255
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* registry-parse.ts — pure, no-I/O parser for `aeg-root/enforcement.md`'s
|
|
3
|
+
* three ring markdown tables (Ring 0/1/2). enforcement.md's tables ARE the
|
|
4
|
+
* gate registry (no live file duplicates forge state); this
|
|
5
|
+
* parses them rather than maintaining a second copy.
|
|
6
|
+
*
|
|
7
|
+
* The generic markdown-table extraction lives in `./markdown-table` — the one
|
|
8
|
+
* aeg-core-owned parser every doctrine consumer shares. This module
|
|
9
|
+
* imports `findTable`/`findHeadingLine` from there rather than re-deriving
|
|
10
|
+
* them; it adds only the enforcement-specific normalization on top (the
|
|
11
|
+
* ring-heading list, `stripBackticks`, and the last-two-columns rule).
|
|
12
|
+
*
|
|
13
|
+
* Each ring's table has a different header wording (Ring 0: Action/Gate/...;
|
|
14
|
+
* Ring 1: CI check/Re-verifies; Ring 2: Mechanism/Runs/Catches). The FIRST
|
|
15
|
+
* column is always the row's identifying label; `implementation` and
|
|
16
|
+
* `description` are resolved BY HEADER NAME, since those two are spelled
|
|
17
|
+
* identically in all three tables. Positional reads are used only where no
|
|
18
|
+
* shared header name exists.
|
|
19
|
+
*
|
|
20
|
+
* The one exception is `Description`, which IS looked up by header name: it is
|
|
21
|
+
* the only column spelled identically in all three tables, and the only one a
|
|
22
|
+
* table can legitimately lack. Position cannot express that difference (a
|
|
23
|
+
* 7-column table means two different shapes depending on the ring), so the
|
|
24
|
+
* name is the only honest key. See the lookup in `parseEnforcementRegistry`.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { findHeadingLine, findTable } from './markdown-table'
|
|
28
|
+
|
|
29
|
+
export type GateRing = 'ring0' | 'ring1' | 'ring2'
|
|
30
|
+
|
|
31
|
+
export type GateRow = {
|
|
32
|
+
ring: GateRing
|
|
33
|
+
action: string
|
|
34
|
+
summary: string
|
|
35
|
+
category: 'ci' | 'hook' | 'event'
|
|
36
|
+
/** One plain sentence: what this gate does, for a reader. Present in all
|
|
37
|
+
* three ring tables and resolved by header name (see the lookup below).
|
|
38
|
+
*
|
|
39
|
+
* The counterpart to `spec`, and not interchangeable with it — `spec` is
|
|
40
|
+
* written to ENFORCE: 17 of the 31 real rows cite an Issue number, a
|
|
41
|
+
* task number or a file path, and the longest runs 2708 chars. That is
|
|
42
|
+
* correct for a gate and unreadable on a page. This column is the same fact
|
|
43
|
+
* addressed to a person, and is what a row's `summary` question gets
|
|
44
|
+
* answered by. The same bar `roles/*.md`/`contracts/*.md` hold via
|
|
45
|
+
* `description:` frontmatter and `ACTIONS` holds via its `description`
|
|
46
|
+
* field, so a reader gets one register whatever they click.
|
|
47
|
+
*
|
|
48
|
+
* Optional on the type only so a malformed table degrades instead of
|
|
49
|
+
* throwing — every real row carries one, asserted against the live file. */
|
|
50
|
+
description?: string
|
|
51
|
+
/** The substantive middle column — "What must be true..." (ring0),
|
|
52
|
+
* "Re-verifies" (ring1), "Catches" (ring2) — always the column
|
|
53
|
+
* immediately before `implementation`. Each ring names it differently,
|
|
54
|
+
* but structurally it's always the same slot: the normative spec, written
|
|
55
|
+
* for enforcement. Distinct from `summary`'s rhetorical question and from
|
|
56
|
+
* `description`'s plain-language answer to it. Undefined when a table has
|
|
57
|
+
* no such column. */
|
|
58
|
+
spec?: string
|
|
59
|
+
implementation: string
|
|
60
|
+
/**
|
|
61
|
+
* `product` — this row's implementation is a check registered in
|
|
62
|
+
* `coreCheckRegistry()` (`apps/cli/src/checks/registry.ts`), so it ships to
|
|
63
|
+
* every adopter through `vinaya check`. `repo-own` — everything else: a
|
|
64
|
+
* mechanism specific to how this repository enforces itself on top of the
|
|
65
|
+
* product (a hand-written CI job, the check runner/resolver, a forge-write
|
|
66
|
+
* command, or a check this repo runs on its own doctrine but has not
|
|
67
|
+
* registered). Read from an `Audience` column, by header name like
|
|
68
|
+
* `description`. A table carrying no such column, or a cell holding
|
|
69
|
+
* anything other than the literal `product`, resolves to `repo-own` — the
|
|
70
|
+
* safe default, since it makes no shipped claim for G6 to verify.
|
|
71
|
+
*/
|
|
72
|
+
audience: 'product' | 'repo-own'
|
|
73
|
+
line: number
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const RING_HEADINGS: Array<{ ring: GateRing; pattern: RegExp }> = [
|
|
77
|
+
{ ring: 'ring0', pattern: /^##\s+Ring 0\b/ },
|
|
78
|
+
{ ring: 'ring1', pattern: /^##\s+Ring 1\b/ },
|
|
79
|
+
{ ring: 'ring2', pattern: /^##\s+Ring 2\b/ }
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
function stripBackticks(cell: string): string {
|
|
83
|
+
const trimmed = cell.trim()
|
|
84
|
+
if (trimmed.length >= 2 && trimmed.startsWith('`') && trimmed.endsWith('`')) {
|
|
85
|
+
return trimmed.slice(1, -1)
|
|
86
|
+
}
|
|
87
|
+
return trimmed
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Strips markdown `**bold**` markers from a gate/check row's own name.
|
|
91
|
+
* `action` is the one cell this parser has never sanitized — most rows are
|
|
92
|
+
* plain text, but a real row can legitimately bold its name for doctrine-
|
|
93
|
+
* prose emphasis, and that literal `**...**` was leaking straight through
|
|
94
|
+
* to `DiagramNode.label` (visible asterisks in the how-it-works UI; every
|
|
95
|
+
* label already renders bold via CSS where it matters, so the markdown
|
|
96
|
+
* marker carries zero information downstream). Root-cause fix, not a
|
|
97
|
+
* per-row doctrine patch — the next accidentally-bolded name is covered
|
|
98
|
+
* too, not just the ones caught so far. */
|
|
99
|
+
function stripBold(cell: string): string {
|
|
100
|
+
return cell.trim().replace(/\*\*/g, '')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Parses the three ring tables out of `enforcement.md`'s raw content into a
|
|
105
|
+
* flat, normalized `GateRow[]`. Each row's `action` is its table's first
|
|
106
|
+
* column; `implementation` is resolved by header name, falling back to the
|
|
107
|
+
* last column.
|
|
108
|
+
*/
|
|
109
|
+
export function parseEnforcementRegistry(content: string): GateRow[] {
|
|
110
|
+
const lines = content.split('\n')
|
|
111
|
+
const result: GateRow[] = []
|
|
112
|
+
|
|
113
|
+
for (const { ring, pattern } of RING_HEADINGS) {
|
|
114
|
+
const headingLine = findHeadingLine(lines, pattern)
|
|
115
|
+
if (headingLine === null) continue
|
|
116
|
+
const table = findTable(lines, headingLine + 1)
|
|
117
|
+
if (!table) continue
|
|
118
|
+
// `description`/`Audience` are found BY HEADER NAME, never by position —
|
|
119
|
+
// the two columns here that can be. Every other column is positional out
|
|
120
|
+
// of necessity: the three ring tables name their first column
|
|
121
|
+
// differently ("Action"/"CI check"/"Mechanism") and their middle columns
|
|
122
|
+
// differently again ("Gate" + "What must be true" / "Re-verifies" /
|
|
123
|
+
// "Runs" + "Catches"), so only the ends are reliable. "Description" and
|
|
124
|
+
// "Audience" are spelled the same in all three, which makes a name
|
|
125
|
+
// lookup possible — and a name lookup is what keeps a table WITHOUT the
|
|
126
|
+
// column from having some other column silently read as its value. By
|
|
127
|
+
// index that is undetectable: a 7-column table means "has Description"
|
|
128
|
+
// in one ring and "has Gate" in another, and the parser cannot tell
|
|
129
|
+
// which. -1 here simply means the table doesn't have that column.
|
|
130
|
+
const descriptionIndex = table.headers.findIndex((h) => h.trim().toLowerCase() === 'description')
|
|
131
|
+
const implementationIndex = table.headers.findIndex((h) => h.trim().toLowerCase() === 'implementation')
|
|
132
|
+
const audienceIndex = table.headers.findIndex((h) => h.trim().toLowerCase() === 'audience')
|
|
133
|
+
for (const row of table.rows) {
|
|
134
|
+
const cells = row.cells
|
|
135
|
+
if (cells.length < 3) continue
|
|
136
|
+
const action = stripBold(cells[0] ?? '')
|
|
137
|
+
const summary = stripBackticks(cells[1] ?? '')
|
|
138
|
+
const category = stripBackticks(cells[2] ?? '') as GateRow['category']
|
|
139
|
+
const implementation = stripBackticks(
|
|
140
|
+
(implementationIndex === -1 ? cells[cells.length - 1] : cells[implementationIndex]) ?? ''
|
|
141
|
+
)
|
|
142
|
+
const description =
|
|
143
|
+
descriptionIndex === -1 ? undefined : stripBackticks(cells[descriptionIndex] ?? '') || undefined
|
|
144
|
+
// `spec` is the ring-specific column immediately before `implementation`
|
|
145
|
+
// — normally the last cell, but `Audience`, when present, now sits
|
|
146
|
+
// between them. `specIndex` follows whichever of the two comes first:
|
|
147
|
+
// one before `Audience` if the table carries one, one before the last
|
|
148
|
+
// cell (`implementation`'s positional fallback) if it doesn't — so a
|
|
149
|
+
// table predating this column (a fixture, an un-upgraded adopter copy)
|
|
150
|
+
// still resolves `spec` exactly as it did before `Audience` existed.
|
|
151
|
+
const specIndex = (audienceIndex === -1 ? cells.length - 1 : audienceIndex) - 1
|
|
152
|
+
const spec = specIndex >= 3 ? stripBackticks(cells[specIndex] ?? '') || undefined : undefined
|
|
153
|
+
const audience: GateRow['audience'] =
|
|
154
|
+
audienceIndex === -1
|
|
155
|
+
? 'repo-own'
|
|
156
|
+
: stripBackticks(cells[audienceIndex] ?? '') === 'product'
|
|
157
|
+
? 'product'
|
|
158
|
+
: 'repo-own'
|
|
159
|
+
result.push({ ring, action, summary, category, description, spec, implementation, audience, line: row.line })
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return result
|
|
164
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* registry-scaffold.ts — pure insertion logic that turns a G2 "orphan
|
|
3
|
+
* hook/CLI" finding into a stub row, instead of leaving it a drift warning a
|
|
4
|
+
* human has to notice and fix by hand.
|
|
5
|
+
*
|
|
6
|
+
* Purity contract identical to `registry-checks.ts`'s: no fs/git/gh here.
|
|
7
|
+
* The caller (`bin/verify-registry.ts`) supplies the parsed rows, the
|
|
8
|
+
* already-globbed candidate files, and the raw `enforcement.md` content to
|
|
9
|
+
* rewrite; this module only ever reads `GATE_AUDIENCE` (a same-package,
|
|
10
|
+
* aeg-core-local data table — see its own doc comment for why aeg-core
|
|
11
|
+
* cannot instead import `apps/cli`'s `CORE_CHECK_RING` directly).
|
|
12
|
+
*
|
|
13
|
+
* Three derivation classes, per Issue #104's Principal decision (extended
|
|
14
|
+
* by Issue #307 for the second registry-backed location):
|
|
15
|
+
* - a `packages/aeg-core/bin/*.ts` candidate that IS a registered check
|
|
16
|
+
* (its basename resolves in `GATE_AUDIENCE` to a `ShippedGate`) gets a
|
|
17
|
+
* stub in the ring `GATE_AUDIENCE` names for it;
|
|
18
|
+
* - an `apps/cli/src/checks/bin/*.ts` candidate that IS a registered check
|
|
19
|
+
* (its `check-`-stripped basename resolves in `CLI_CHECK_RING`) gets a
|
|
20
|
+
* stub in the ring `CLI_CHECK_RING` names for it — most core check bins
|
|
21
|
+
* live here, not under `packages/aeg-core/bin/` (Issue #307: 22 of 27 at
|
|
22
|
+
* the time it was filed);
|
|
23
|
+
* - a `.husky/*` or `.claude/hooks/*.sh` candidate is a managed hook by
|
|
24
|
+
* construction — ring 0, no registry lookup needed.
|
|
25
|
+
* Anything else (a bin with no registry entry — `internal`, `NON_GATE_BINS`,
|
|
26
|
+
* or simply undeclared) gets NO stub: ring is genuinely underivable there,
|
|
27
|
+
* and guessing is exactly what this task's Stop-and-escalate condition
|
|
28
|
+
* forbids. It stays a plain G2 finding, same as today.
|
|
29
|
+
*
|
|
30
|
+
* Never touches an existing row: stubs are computed only for candidates
|
|
31
|
+
* whose path is absent from every parsed row's `implementation` — the
|
|
32
|
+
* identical predicate `checkG2` itself uses — so a candidate already
|
|
33
|
+
* documented (including every row `#67` wrote by hand) is left alone, and
|
|
34
|
+
* running the scaffold twice inserts nothing the second time (the first
|
|
35
|
+
* run's own stubs are now present, so the same predicate excludes them).
|
|
36
|
+
*
|
|
37
|
+
* Only mechanical facts are filled: the check name / candidate path
|
|
38
|
+
* (`Action`/`CI check`/`Mechanism`), `Category` (fixed per ring — matches
|
|
39
|
+
* every existing row: ring 0 is `hook`, ring 1 is `ci`, ring 2 is `event`),
|
|
40
|
+
* `Audience` (`product` for a registered check, `repo-own` for a bare hook
|
|
41
|
+
* script), and `implementation` (the path itself). A ring-0 stub also gets a
|
|
42
|
+
* mechanical `Gate` cell naming the check/its invocation — never the
|
|
43
|
+
* substantive "what must be true"/"re-verifies"/"catches" reasoning, which
|
|
44
|
+
* (along with `Summary`/`Description`) is the placeholder marker, verbatim,
|
|
45
|
+
* on every stub this module ever produces.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import { CLI_CHECK_RING, GATE_AUDIENCE, isShipped } from './gate-audience'
|
|
49
|
+
import { findHeadingLine, findTable } from './markdown-table'
|
|
50
|
+
import type { GateRing, GateRow } from './registry-parse'
|
|
51
|
+
|
|
52
|
+
/** The literal marker a stub's every non-mechanical cell carries. Never
|
|
53
|
+
* synthesized reasoning — `checkG2`'s placeholder scan (registry-checks.ts)
|
|
54
|
+
* looks for this exact string. */
|
|
55
|
+
export const PLACEHOLDER = '[undocumented — fill in why]'
|
|
56
|
+
|
|
57
|
+
const HUSKY_PREFIX = '.husky/'
|
|
58
|
+
const CLAUDE_HOOKS_PREFIX = '.claude/hooks/'
|
|
59
|
+
const AEG_CORE_BIN_PREFIX = 'packages/aeg-core/bin/'
|
|
60
|
+
const CLI_BIN_PREFIX = 'apps/cli/src/checks/bin/'
|
|
61
|
+
|
|
62
|
+
const RING_CATEGORY: Record<GateRing, string> = { ring0: 'hook', ring1: 'ci', ring2: 'event' }
|
|
63
|
+
|
|
64
|
+
const RING_HEADING_PATTERN: Record<GateRing, RegExp> = {
|
|
65
|
+
ring0: /^##\s+Ring 0\b/,
|
|
66
|
+
ring1: /^##\s+Ring 1\b/,
|
|
67
|
+
ring2: /^##\s+Ring 2\b/
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type ScaffoldStub = {
|
|
71
|
+
ring: GateRing
|
|
72
|
+
path: string
|
|
73
|
+
checkName?: string
|
|
74
|
+
/** Ordered cell values, matching that ring table's own column count/order. */
|
|
75
|
+
cells: string[]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type ScaffoldSkip = { path: string; reason: string }
|
|
79
|
+
|
|
80
|
+
export type ScaffoldPlan = {
|
|
81
|
+
stubs: ScaffoldStub[]
|
|
82
|
+
skipped: ScaffoldSkip[]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function ringFromNumber(n: 0 | 1 | 2): GateRing {
|
|
86
|
+
if (n === 0) return 'ring0'
|
|
87
|
+
if (n === 1) return 'ring1'
|
|
88
|
+
return 'ring2'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Classifies one G2-orphan candidate path. Returns null when no ring is
|
|
92
|
+
* derivable — the candidate must remain a plain G2 finding, never guessed. */
|
|
93
|
+
function classify(path: string): { ring: GateRing; checkName?: string } | null {
|
|
94
|
+
if (path.startsWith(HUSKY_PREFIX) || path.startsWith(CLAUDE_HOOKS_PREFIX)) {
|
|
95
|
+
return { ring: 'ring0' }
|
|
96
|
+
}
|
|
97
|
+
if (path.startsWith(AEG_CORE_BIN_PREFIX) && path.endsWith('.ts')) {
|
|
98
|
+
const base = path.slice(AEG_CORE_BIN_PREFIX.length, -'.ts'.length)
|
|
99
|
+
const audience = GATE_AUDIENCE[base]
|
|
100
|
+
if (audience && isShipped(audience)) {
|
|
101
|
+
const checkName = Array.isArray(audience.shippedAs) ? audience.shippedAs[0] : audience.shippedAs
|
|
102
|
+
return { ring: ringFromNumber(audience.ring), checkName }
|
|
103
|
+
}
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
if (path.startsWith(CLI_BIN_PREFIX) && path.endsWith('.ts')) {
|
|
107
|
+
const base = path.slice(CLI_BIN_PREFIX.length, -'.ts'.length)
|
|
108
|
+
if (!base.startsWith('check-')) return null
|
|
109
|
+
const checkName = base.slice('check-'.length)
|
|
110
|
+
const ring = CLI_CHECK_RING[checkName]
|
|
111
|
+
if (ring === undefined) return null
|
|
112
|
+
return { ring: ringFromNumber(ring), checkName }
|
|
113
|
+
}
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function gateCell(path: string, checkName: string | undefined): string {
|
|
118
|
+
if (checkName) return `\`${checkName}\` check (\`vinaya check ${checkName}\`)`
|
|
119
|
+
return `managed hook script (\`${path}\`)`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildCells(ring: GateRing, path: string, checkName: string | undefined): string[] {
|
|
123
|
+
const action = checkName ?? path
|
|
124
|
+
const category = RING_CATEGORY[ring]
|
|
125
|
+
const audience = checkName ? 'product' : 'repo-own'
|
|
126
|
+
const implementation = `\`${path}\``
|
|
127
|
+
if (ring === 'ring0') {
|
|
128
|
+
return [
|
|
129
|
+
action,
|
|
130
|
+
PLACEHOLDER,
|
|
131
|
+
category,
|
|
132
|
+
PLACEHOLDER,
|
|
133
|
+
gateCell(path, checkName),
|
|
134
|
+
PLACEHOLDER,
|
|
135
|
+
audience,
|
|
136
|
+
implementation
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
if (ring === 'ring1') {
|
|
140
|
+
return [action, PLACEHOLDER, category, PLACEHOLDER, PLACEHOLDER, audience, implementation]
|
|
141
|
+
}
|
|
142
|
+
return [action, PLACEHOLDER, category, PLACEHOLDER, PLACEHOLDER, PLACEHOLDER, audience, implementation]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Computes which G2-orphan candidates get a stub row, and where. Pure: no
|
|
146
|
+
* I/O, deterministic over its inputs. Idempotent by construction — a
|
|
147
|
+
* candidate already present in `rows` (including one from a prior scaffold
|
|
148
|
+
* run) is excluded before classification ever runs. */
|
|
149
|
+
export function computeScaffoldPlan(rows: GateRow[], candidateFiles: string[]): ScaffoldPlan {
|
|
150
|
+
const implementations = new Set(rows.map((r) => r.implementation).filter((p) => p !== ''))
|
|
151
|
+
const stubs: ScaffoldStub[] = []
|
|
152
|
+
const skipped: ScaffoldSkip[] = []
|
|
153
|
+
for (const path of candidateFiles) {
|
|
154
|
+
if (implementations.has(path)) continue
|
|
155
|
+
const classified = classify(path)
|
|
156
|
+
if (!classified) {
|
|
157
|
+
skipped.push({ path, reason: 'no derivable ring — remains a plain G2 finding' })
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
stubs.push({
|
|
161
|
+
ring: classified.ring,
|
|
162
|
+
path,
|
|
163
|
+
checkName: classified.checkName,
|
|
164
|
+
cells: buildCells(classified.ring, path, classified.checkName)
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
return { stubs, skipped }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function formatRow(cells: string[]): string {
|
|
171
|
+
return `| ${cells.join(' | ')} |`
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Rewrites `content` to append the plan's stub rows to their ring tables.
|
|
176
|
+
* Only ever APPENDS new lines after a ring table's last existing row —
|
|
177
|
+
* never edits, reorders, or removes an existing line, so a hand-written row
|
|
178
|
+
* (including every row task `#67` wrote) is untouched byte-for-byte.
|
|
179
|
+
*
|
|
180
|
+
* Rings are processed from the bottom of the file upward (ring 2 first,
|
|
181
|
+
* then ring 1, then ring 0) so an earlier insertion's line numbers, computed
|
|
182
|
+
* once up front from the ORIGINAL `rows`, stay valid throughout — a later
|
|
183
|
+
* ring's insertion never shifts an earlier ring's already-resolved splice
|
|
184
|
+
* point.
|
|
185
|
+
*
|
|
186
|
+
* Throws if a ring with stubs to insert has no existing table/rows to
|
|
187
|
+
* anchor on — the caller (`bin/verify-registry.ts`) must not write anything
|
|
188
|
+
* to disk when this throws; computing this in memory before any write is
|
|
189
|
+
* what keeps the round-trip guard restore-free (nothing is ever written
|
|
190
|
+
* badly in the first place).
|
|
191
|
+
*/
|
|
192
|
+
export function applyScaffoldPlan(content: string, plan: ScaffoldPlan): string {
|
|
193
|
+
if (plan.stubs.length === 0) return content
|
|
194
|
+
const lines = content.split('\n')
|
|
195
|
+
|
|
196
|
+
const byRing: Record<GateRing, ScaffoldStub[]> = { ring0: [], ring1: [], ring2: [] }
|
|
197
|
+
for (const stub of plan.stubs) byRing[stub.ring].push(stub)
|
|
198
|
+
|
|
199
|
+
for (const ring of ['ring2', 'ring1', 'ring0'] as const) {
|
|
200
|
+
const stubs = byRing[ring]
|
|
201
|
+
if (stubs.length === 0) continue
|
|
202
|
+
const headingLine = findHeadingLine(lines, RING_HEADING_PATTERN[ring])
|
|
203
|
+
if (headingLine === null) {
|
|
204
|
+
throw new Error(`registry-scaffold: no "${ring}" heading found in enforcement.md`)
|
|
205
|
+
}
|
|
206
|
+
const table = findTable(lines, headingLine + 1)
|
|
207
|
+
if (!table || table.rows.length === 0) {
|
|
208
|
+
throw new Error(`registry-scaffold: ${ring} table has no existing rows to insert after`)
|
|
209
|
+
}
|
|
210
|
+
const lastRowLine = table.rows[table.rows.length - 1]?.line ?? headingLine
|
|
211
|
+
const newLines = stubs.map((s) => formatRow(s.cells))
|
|
212
|
+
lines.splice(lastRowLine, 0, ...newLines)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return lines.join('\n')
|
|
216
|
+
}
|