@attalabs/vinaya 0.21.0 → 0.23.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 +15 -4
- 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 +28 -21
- 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 +2 -2
- 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 +12 -7
- package/aeg-root/tranche-model.md +9 -5
- package/dist/checks/bin/check-body-bare-digits.js +667 -148
- package/dist/checks/bin/check-branch-topology.js +714 -149
- package/dist/checks/bin/check-brief-shape.js +704 -147
- package/dist/checks/bin/check-changeset-coverage.js +1046 -166
- package/dist/checks/bin/check-closes-n.js +714 -149
- package/dist/checks/bin/check-coherence.js +729 -156
- package/dist/checks/bin/check-dead-branch-push.js +663 -144
- package/dist/checks/bin/check-dispatch-readiness.js +728 -155
- package/dist/checks/bin/check-doc-coverage-push.js +838 -261
- package/dist/checks/bin/check-doc-coverage.js +838 -261
- package/dist/checks/bin/check-doctrine-no-procedures.js +4794 -0
- package/dist/checks/bin/check-doctrine-portability.js +799 -155
- package/dist/checks/bin/check-evidence-fresh.js +1132 -206
- package/dist/checks/bin/check-exec-bits.js +4931 -0
- package/dist/checks/bin/check-first-push-dispatch.js +716 -151
- package/dist/checks/bin/check-issue-assignment.js +714 -149
- package/dist/checks/bin/check-main-branch-refusal.js +663 -144
- package/dist/checks/bin/check-no-disk-state.js +663 -144
- package/dist/checks/bin/check-pr-report-density.js +4461 -0
- package/dist/checks/bin/check-quoted-command.js +741 -166
- package/dist/checks/bin/check-reader-resolvable-prose.js +768 -173
- package/dist/checks/bin/check-registry-gates.js +726 -154
- package/dist/checks/bin/check-retired-vocabulary.js +760 -165
- package/dist/checks/bin/check-review-gate.js +783 -148
- package/dist/checks/bin/check-single-plan-pr.js +663 -144
- package/dist/checks/bin/check-test-plan.js +664 -145
- package/dist/checks/bin/check-token-collection-wired.js +665 -146
- package/dist/checks/bin/check-token-report.js +4509 -0
- package/dist/checks/bin/check-workspace-escape.js +1129 -183
- package/dist/index.js +8458 -5403
- 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 +113 -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 +192 -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-validation.ts +511 -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 +787 -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 +224 -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-portability.ts +170 -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 +168 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/index.ts +301 -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 +165 -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-tier.ts +81 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/premise-check.ts +125 -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 +269 -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 +104 -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 +140 -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]__02c71kv._.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]__0ozbaq0._.js → [root-of-the-server]__1mcitst._.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/_0o54suy._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1-6rzip._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1mrtj0f._.js +1 -1
- 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_06it6s-._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_animate_components_index_ts_186fje1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_brutal_components_index_ts_0n_lcc4._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_retro_components_index_ts_0wvpm_z._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_retro_components_index_ts_1_cfi48._.js +1 -1
- 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/00pi8vhg8w6gi.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0r4i_ks9z02bw.js → 03q0q9n6-xah8.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2_k6j1o9q5nn4.js → 0jer_drkl1e4r.js} +4 -4
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0vc2u252q5q6o.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1k916ih3p4nsg.js → 13g16cu00g570.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2hmannd4bjhzy.js → 1sfxfunirkcr6.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1t2kft7bxzuxn.js → 206ajg8f1a-_h.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1hfhxjy577l5d.js → 20x-t0a0v129z.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{25kbt5k-xb2a5.js → 279qq4twinc97.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{112g08xpx3kro.js → 2ka8dg687auhj.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/2ou_81ahqw31e.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0up5x3jg-cwov.js → 2rn89uwtf2lwc.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3mqu_uhluy19i.css → 3qc67qhcvbg0o.css} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1yqs2zgvwa4p1.js → 3zugx9esh2x51.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0_jnwbarv-cqk.js +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1ybrihl5_aocy.js +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3r5efmleneaco.js +0 -1
- /package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{crNCHQ9uAgKOYC6DKkZma → 5xy_GA7G5KRuE3HDumIiw}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{crNCHQ9uAgKOYC6DKkZma → 5xy_GA7G5KRuE3HDumIiw}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{crNCHQ9uAgKOYC6DKkZma → 5xy_GA7G5KRuE3HDumIiw}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader-resolvable prose — classes 1 and 2 of the three-class analysis in
|
|
3
|
+
* Issue #694: the two mechanizable classes of "words a reader cannot
|
|
4
|
+
* resolve." Class 3 (register/slop — padding adjectives, first person,
|
|
5
|
+
* narrating the work episode) is NOT here; it stays with the review role,
|
|
6
|
+
* per the measurement below.
|
|
7
|
+
*
|
|
8
|
+
* **The measurement that disqualifies a phrase blacklist.** A phrase
|
|
9
|
+
* blacklist tested against the real corpus (all of `aeg-root` plus 1,157
|
|
10
|
+
* TypeScript files) scored 14 hits, 14 false positives, 0 true positives —
|
|
11
|
+
* "a review was *requested*"; "could *this change* leak a secret"; "*I have*
|
|
12
|
+
* X, Y, Z" inside a quoted example. The fix is not tuning the word list; it
|
|
13
|
+
* is asking a different question:
|
|
14
|
+
*
|
|
15
|
+
* - Class 1 (references): not "does this phrase appear" but "does this
|
|
16
|
+
* doctrine cite a forge number or tranche slug the reader has no forge to
|
|
17
|
+
* resolve." Reuses the same two pattern shapes proven in
|
|
18
|
+
* `retired-vocabulary.test.ts` (`FORGE_NUMBER_PATTERN`,
|
|
19
|
+
* `TRANCHE_SLUG_VN_PATTERN`, and the legacy-slug list derived — not
|
|
20
|
+
* guessed — from `aeg-root/tranches/completed/*.md` filenames), scoped to
|
|
21
|
+
* the reader-facing surfaces instead of banned repo-wide (the same digit
|
|
22
|
+
* shape is this product's own live `Closes #N`/PR-number grammar
|
|
23
|
+
* elsewhere, so a repo-wide ban would flag the mechanism, not the
|
|
24
|
+
* citation habit).
|
|
25
|
+
* - Class 2 (vocabulary): not "is this word forbidden" but "does this
|
|
26
|
+
* reader-facing file use a coined term while neither defining it inline
|
|
27
|
+
* nor linking a definition." The term list is derived from
|
|
28
|
+
* `aeg-root/glossary.md`'s own entry headings — never hard-coded — so it
|
|
29
|
+
* cannot drift the first time an entry is added.
|
|
30
|
+
*
|
|
31
|
+
* Zero I/O: every input (file paths + contents, the glossary's term list, the
|
|
32
|
+
* legacy-slug list) is read by the adapter and passed in. POSIX-ERE-safe
|
|
33
|
+
* patterns only — no `\d`/`\w`/`\s`/lookaheads, and no `\b` relied on after a
|
|
34
|
+
* non-word character (the exact regression class `retired-vocabulary.test.ts`
|
|
35
|
+
* already paid for) — even though these patterns run through native
|
|
36
|
+
* `RegExp`, not `grep -E`, keeping them portable to either.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export type ProseFileClass = 'ships' | 'reader-facing' | 'internal'
|
|
40
|
+
|
|
41
|
+
export type ProseSourceFile = { path: string; content: string }
|
|
42
|
+
|
|
43
|
+
export type ProseFinding = {
|
|
44
|
+
file: string
|
|
45
|
+
line: number
|
|
46
|
+
message: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `aeg-root/**` by default — what this repo's own package carries and
|
|
51
|
+
* `/docs` publishes. Every caller below takes `shipsPrefix` as its LAST,
|
|
52
|
+
* defaulted parameter so an adopter whose installed doctrine tree lives
|
|
53
|
+
* somewhere else (task 7, Issue #56 — de-hardcoding `doctrineRoot`) can
|
|
54
|
+
* override it without breaking any existing positional call.
|
|
55
|
+
*/
|
|
56
|
+
const SHIPS_PREFIX = 'aeg-root/'
|
|
57
|
+
|
|
58
|
+
/** `<shipsPrefix>tranches/completed/` — history, never rewritten. Same exemption `retired-vocabulary.test.ts` already carries for the identical reason: an archived tranche legitimately cites the forge numbers and slugs it closed. */
|
|
59
|
+
function shipsArchivePrefix(shipsPrefix: string): string {
|
|
60
|
+
return `${shipsPrefix}tranches/completed/`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A per-product specs file (`apps/<product>/specs/**`) — this reader has this forge; references are legitimate here. */
|
|
64
|
+
function isSpecFile(path: string): boolean {
|
|
65
|
+
return path.startsWith('apps/') && path.includes('/specs/') && path.endsWith('.md')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Any `CLAUDE.md`, at any depth — same reasoning as specs: this reader has this forge. */
|
|
69
|
+
function isClaudeMdFile(path: string): boolean {
|
|
70
|
+
return path === 'CLAUDE.md' || path.endsWith('/CLAUDE.md')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The file-class map, encoded as data (not a rule buried in control flow).
|
|
75
|
+
* Returns `null` for anything outside all three classes — out of this
|
|
76
|
+
* check's scope entirely, not merely exempt.
|
|
77
|
+
*
|
|
78
|
+
* `readerFacingPrefix`/`readerFacingSuffix` scope the "reader-facing" class
|
|
79
|
+
* to a consumer's own reader-facing surface — this package stays generic and
|
|
80
|
+
* has no attalabs-specific path baked in. The caller-supplied shape mirrors
|
|
81
|
+
* the vinaya adopter's product site: a page's own body is where
|
|
82
|
+
* reader-visible prose lives (JSX text, step copy); sibling component
|
|
83
|
+
* directories include non-textual/decorative surfaces (e.g. canvas
|
|
84
|
+
* illustrations using fictional PR numbers as flavor text) that are not
|
|
85
|
+
* prose in the sense this check means, hence a suffix-scoped match rather
|
|
86
|
+
* than a bare prefix.
|
|
87
|
+
*/
|
|
88
|
+
export function classifyProseFile(
|
|
89
|
+
path: string,
|
|
90
|
+
readerFacingPrefix: string,
|
|
91
|
+
readerFacingSuffix: string,
|
|
92
|
+
shipsPrefix: string = SHIPS_PREFIX
|
|
93
|
+
): ProseFileClass | null {
|
|
94
|
+
if (path.startsWith(shipsPrefix)) {
|
|
95
|
+
return path.startsWith(shipsArchivePrefix(shipsPrefix)) ? 'internal' : 'ships'
|
|
96
|
+
}
|
|
97
|
+
if (path.startsWith(readerFacingPrefix) && path.endsWith(readerFacingSuffix)) {
|
|
98
|
+
return 'reader-facing'
|
|
99
|
+
}
|
|
100
|
+
if (isSpecFile(path) || isClaudeMdFile(path)) return 'internal'
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The two classes this check actually sweeps — `internal` never is. */
|
|
105
|
+
const SWEPT_CLASSES: ReadonlySet<ProseFileClass> = new Set(['ships', 'reader-facing'])
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Strips the parts of a file that are never reader prose, per file kind — a
|
|
109
|
+
* fenced/inline code example in a markdown doc, or a source comment in a
|
|
110
|
+
* `.tsx` page (a page's own developer-facing comments, e.g. "same reasoning
|
|
111
|
+
* as `/roadmap`", are not what a site visitor reads). Line count is
|
|
112
|
+
* preserved (replacements keep their newlines) so reported line numbers stay
|
|
113
|
+
* accurate against the original file.
|
|
114
|
+
*
|
|
115
|
+
* The `//` line-comment cut requires the marker not be immediately preceded
|
|
116
|
+
* by `:` — without that, `https://vinaya.dev` on a reader-facing line reads
|
|
117
|
+
* as a comment starting at its own `//`, silently blanking every word of
|
|
118
|
+
* real prose that follows it on the line. No boundary check is needed on
|
|
119
|
+
* the far side: unlike `//`, the character before it is never itself part
|
|
120
|
+
* of the marker, so a plain capturing group (not a lookbehind) is enough.
|
|
121
|
+
*/
|
|
122
|
+
export function stripNonProse(path: string, content: string): string {
|
|
123
|
+
if (path.endsWith('.md')) {
|
|
124
|
+
return content
|
|
125
|
+
.replace(/```.*?```/gs, (m) => m.replace(/[^\n]/g, ''))
|
|
126
|
+
.replace(/`[^`\n]*`/g, (m) => m.replace(/[^\n]/g, ''))
|
|
127
|
+
}
|
|
128
|
+
if (path.endsWith('.tsx') || path.endsWith('.ts')) {
|
|
129
|
+
return content
|
|
130
|
+
.replace(/\/\*.*?\*\//gs, (m) => m.replace(/[^\n]/g, ''))
|
|
131
|
+
.replace(/(^|[^:])\/\/[^\n]*/g, (m) => m.replace(/[^\n]/g, ''))
|
|
132
|
+
}
|
|
133
|
+
return content
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function lineAt(content: string, index: number): number {
|
|
137
|
+
let line = 1
|
|
138
|
+
for (let i = 0; i < index; i++) {
|
|
139
|
+
if (content.charCodeAt(i) === 10) line++
|
|
140
|
+
}
|
|
141
|
+
return line
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** `#294`-shaped — a PR/Issue number cited bare, resolvable only inside this repo's own tracker. */
|
|
145
|
+
export const FORGE_NUMBER_PATTERN = /#[0-9]{2,4}/g
|
|
146
|
+
|
|
147
|
+
/** `aeg-forge-state-v1`-shaped — an internal tranche slug ending `-vN`. */
|
|
148
|
+
export const TRANCHE_SLUG_VN_PATTERN = /[a-z][a-z-]+-v[0-9]/g
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Builds the legacy-slug pattern from the real archived-tranche filenames
|
|
152
|
+
* the adapter reads (mirrors `retired-vocabulary.test.ts`'s `legacySlugs()` —
|
|
153
|
+
* derived from `aeg-root/tranches/completed/*.md`, never a shape guess).
|
|
154
|
+
* Returns `null` when there is nothing left for it to catch (every archived
|
|
155
|
+
* slug now ends `-vN`), matching that file's "no pattern with zero positive
|
|
156
|
+
* samples" rule.
|
|
157
|
+
*
|
|
158
|
+
* Boundary-checked like `containsWholeWord` below (character class, not
|
|
159
|
+
* `\b`) so a real slug cannot match as a bare substring inside a longer,
|
|
160
|
+
* unrelated token. `retired-vocabulary.test.ts`'s own copy of this pattern
|
|
161
|
+
* has no such boundary; this one adds it rather than copying the gap.
|
|
162
|
+
* Group 1 is the leading boundary char (or start-of-string), group 2 is the
|
|
163
|
+
* slug itself, group 3 the trailing boundary char (or end-of-string) — the
|
|
164
|
+
* caller reports group 2, not the whole match, so the boundary chars never
|
|
165
|
+
* leak into a finding's message.
|
|
166
|
+
*/
|
|
167
|
+
export function legacySlugPattern(legacySlugs: readonly string[]): RegExp | null {
|
|
168
|
+
if (legacySlugs.length === 0) return null
|
|
169
|
+
const alternation = legacySlugs.map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')
|
|
170
|
+
return new RegExp(`(^|[^a-zA-Z0-9\\n-])(${alternation})([^a-zA-Z0-9\\n-]|$)`, 'g')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Class 1 — unresolvable references. Runs the reference patterns over every
|
|
175
|
+
* swept file (`ships`/`reader-facing`), skipping code-fenced/commented-out
|
|
176
|
+
* non-prose first.
|
|
177
|
+
*/
|
|
178
|
+
export function checkUnresolvableReferences(
|
|
179
|
+
files: readonly ProseSourceFile[],
|
|
180
|
+
readerFacingPrefix: string,
|
|
181
|
+
readerFacingSuffix: string,
|
|
182
|
+
legacySlugs: readonly string[] = [],
|
|
183
|
+
shipsPrefix: string = SHIPS_PREFIX
|
|
184
|
+
): ProseFinding[] {
|
|
185
|
+
const findings: ProseFinding[] = []
|
|
186
|
+
const legacyPattern = legacySlugPattern(legacySlugs)
|
|
187
|
+
// `group` names the capture holding the actual cited text — the plain
|
|
188
|
+
// patterns have none (report the whole match), the boundary-checked
|
|
189
|
+
// legacy-slug pattern reports its group 2 so the boundary chars around it
|
|
190
|
+
// never leak into the finding's message.
|
|
191
|
+
const patterns: { pattern: RegExp; what: string; group?: number }[] = [
|
|
192
|
+
{ pattern: FORGE_NUMBER_PATTERN, what: 'a forge number' },
|
|
193
|
+
{ pattern: TRANCHE_SLUG_VN_PATTERN, what: 'an internal tranche slug' },
|
|
194
|
+
...(legacyPattern ? [{ pattern: legacyPattern, what: 'an internal tranche slug', group: 2 }] : [])
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
for (const file of files) {
|
|
198
|
+
const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix, shipsPrefix)
|
|
199
|
+
if (!cls || !SWEPT_CLASSES.has(cls)) continue
|
|
200
|
+
const scrubbed = stripNonProse(file.path, file.content)
|
|
201
|
+
for (const { pattern, what, group } of patterns) {
|
|
202
|
+
pattern.lastIndex = 0
|
|
203
|
+
let match: RegExpExecArray | null = pattern.exec(scrubbed)
|
|
204
|
+
while (match !== null) {
|
|
205
|
+
const cited = group !== undefined ? (match[group] ?? match[0]) : match[0]
|
|
206
|
+
findings.push({
|
|
207
|
+
file: file.path,
|
|
208
|
+
line: lineAt(scrubbed, match.index),
|
|
209
|
+
message: `references ${what} ("${cited}") a reader outside this repo's tracker cannot resolve`
|
|
210
|
+
})
|
|
211
|
+
match = pattern.exec(scrubbed)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return findings
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Escapes a glossary term for use inside a hand-built, boundary-checked pattern. */
|
|
219
|
+
function escapeTerm(term: string): string {
|
|
220
|
+
return term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Whole-word (or whole-phrase), case-insensitive occurrence of `term` in
|
|
225
|
+
* `text` — boundaries checked by character class rather than `\b`, per the
|
|
226
|
+
* "do not rely on `\b` after a non-word character" lesson. Matches the exact
|
|
227
|
+
* glossary heading only (no inflections): "briefly" must never match
|
|
228
|
+
* "brief" the way a prefix match would — that exact shape (`let's` matching
|
|
229
|
+
* `bullet's`) is what the disqualifying measurement caught.
|
|
230
|
+
*/
|
|
231
|
+
function containsWholeWord(text: string, term: string): boolean {
|
|
232
|
+
const escaped = escapeTerm(term)
|
|
233
|
+
const re = new RegExp(`(^|[^a-zA-Z])${escaped}([^a-zA-Z]|$)`, 'i')
|
|
234
|
+
return re.test(text)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function findWholeWordLine(content: string, term: string): number | null {
|
|
238
|
+
const lines = content.split('\n')
|
|
239
|
+
let lineNo = 0
|
|
240
|
+
for (const line of lines) {
|
|
241
|
+
lineNo++
|
|
242
|
+
if (containsWholeWord(line, term)) return lineNo
|
|
243
|
+
}
|
|
244
|
+
return null
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* A file "defines" a term inline when it carries the same
|
|
249
|
+
* `Term — definition` shape the glossary itself uses (an em-dash right after
|
|
250
|
+
* the term), or links the glossary page/file directly.
|
|
251
|
+
*/
|
|
252
|
+
function definesOrLinksGlossary(content: string, term: string): boolean {
|
|
253
|
+
const escaped = escapeTerm(term)
|
|
254
|
+
const definesInline = new RegExp(`(^|[^a-zA-Z])${escaped}([^a-zA-Z]|$)[^\\n]{0,3}—`, 'i').test(content)
|
|
255
|
+
if (definesInline) return true
|
|
256
|
+
return /docs\/glossary|glossary\.md/i.test(content)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Class 2 — undefined coined vocabulary. Not "is this word forbidden" — it
|
|
261
|
+
* is "does this reader-facing file use a coined term while neither defining
|
|
262
|
+
* it inline nor linking a definition." `glossaryTerms` is read by the
|
|
263
|
+
* adapter from `aeg-root/glossary.md`'s own entry headings — never
|
|
264
|
+
* hard-coded here, so the check cannot silently rot the first time an entry
|
|
265
|
+
* is added.
|
|
266
|
+
*/
|
|
267
|
+
export function checkUndefinedVocabulary(
|
|
268
|
+
files: readonly ProseSourceFile[],
|
|
269
|
+
glossaryTerms: readonly string[],
|
|
270
|
+
readerFacingPrefix: string,
|
|
271
|
+
readerFacingSuffix: string,
|
|
272
|
+
shipsPrefix: string = SHIPS_PREFIX
|
|
273
|
+
): ProseFinding[] {
|
|
274
|
+
const findings: ProseFinding[] = []
|
|
275
|
+
|
|
276
|
+
for (const file of files) {
|
|
277
|
+
const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix, shipsPrefix)
|
|
278
|
+
if (!cls || !SWEPT_CLASSES.has(cls)) continue
|
|
279
|
+
const scrubbed = stripNonProse(file.path, file.content)
|
|
280
|
+
|
|
281
|
+
for (const term of glossaryTerms) {
|
|
282
|
+
const line = findWholeWordLine(scrubbed, term)
|
|
283
|
+
if (line === null) continue
|
|
284
|
+
if (definesOrLinksGlossary(scrubbed, term)) continue
|
|
285
|
+
findings.push({
|
|
286
|
+
file: file.path,
|
|
287
|
+
line,
|
|
288
|
+
message: `uses coined term "${term}" without defining it inline or linking the glossary`
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return findings
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Runs both mechanizable classes over the given files in one pass. */
|
|
296
|
+
export function checkReaderResolvableProse(
|
|
297
|
+
files: readonly ProseSourceFile[],
|
|
298
|
+
glossaryTerms: readonly string[],
|
|
299
|
+
readerFacingPrefix: string,
|
|
300
|
+
readerFacingSuffix: string,
|
|
301
|
+
legacySlugs: readonly string[] = [],
|
|
302
|
+
shipsPrefix: string = SHIPS_PREFIX
|
|
303
|
+
): ProseFinding[] {
|
|
304
|
+
return [
|
|
305
|
+
...checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuffix, legacySlugs, shipsPrefix),
|
|
306
|
+
...checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix, shipsPrefix)
|
|
307
|
+
]
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Parses `aeg-root/glossary.md`'s `**Term** — definition` entry headings into a bare term list. */
|
|
311
|
+
export function parseGlossaryTerms(glossaryContent: string): string[] {
|
|
312
|
+
const terms: string[] = []
|
|
313
|
+
const re = /^\*\*(.+?)\*\* —/gm
|
|
314
|
+
let match: RegExpExecArray | null = re.exec(glossaryContent)
|
|
315
|
+
while (match !== null) {
|
|
316
|
+
if (match[1] !== undefined) terms.push(match[1])
|
|
317
|
+
match = re.exec(glossaryContent)
|
|
318
|
+
}
|
|
319
|
+
return terms
|
|
320
|
+
}
|
|
@@ -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
|
+
}
|