@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,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctrine-no-procedures sweep (task 10, Issue #385) — task 9's rule that
|
|
3
|
+
* doctrine prose describes no command sequence, made a check. `roles/*.md`
|
|
4
|
+
* and `contracts/*.md` explain *what* a command sequence does and *why*;
|
|
5
|
+
* they are not a runbook a reader executes verbatim, and a copy-pasted
|
|
6
|
+
* sequence rots the moment the real command changes (found live: two of
|
|
7
|
+
* this tranche's own PRs were blocked writing exactly this shape after the
|
|
8
|
+
* rule was already known in prose).
|
|
9
|
+
*
|
|
10
|
+
* Zero I/O: every input (file paths + contents) is read by the adapter
|
|
11
|
+
* (`check-doctrine-no-procedures.ts`) and passed in — same discipline as
|
|
12
|
+
* `doctrine-portability.ts` beside it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { COMMAND_WORDS, extractFencedBlocks } from './brief-validation'
|
|
16
|
+
import { VENDOR_EXAMPLE_END, VENDOR_EXAMPLE_START } from './doctrine-portability'
|
|
17
|
+
|
|
18
|
+
export type DoctrineFile = { path: string; content: string }
|
|
19
|
+
export type DoctrineProcedureFinding = { file: string; line: number; message: string }
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The one sanctioned fenced home for a real command sequence in doctrine
|
|
23
|
+
* (`tranche-model.md` §12's `AEG:VENDOR-EXAMPLE` pair) — same markers
|
|
24
|
+
* `doctrine-portability.ts` exempts from its own vendor-name scan, exported
|
|
25
|
+
* from there rather than re-declared here. First pair wins, same rule as
|
|
26
|
+
* every other `AEG:*` anchor consumer.
|
|
27
|
+
*/
|
|
28
|
+
function vendorExampleSpan(content: string): { start: number; end: number } | null {
|
|
29
|
+
const start = VENDOR_EXAMPLE_START.exec(content)
|
|
30
|
+
if (!start) return null
|
|
31
|
+
const afterStart = start.index + start[0].length
|
|
32
|
+
const end = VENDOR_EXAMPLE_END.exec(content.slice(afterStart))
|
|
33
|
+
if (!end) return null
|
|
34
|
+
return { start: start.index, end: afterStart + end.index + end[0].length }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isCommandLine(line: string): boolean {
|
|
38
|
+
const word = line.trim().split(/\s+/)[0] ?? ''
|
|
39
|
+
return (COMMAND_WORDS as readonly string[]).includes(word)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A fence's language tag that reads as shell — the empty tag (` ``` `, most
|
|
44
|
+
* doctrine command blocks) or an explicit `sh`/`bash`/`shell`/`console`.
|
|
45
|
+
* Anything else (`ts`, `js`, `yaml`, `json`, …) is a real illustration in
|
|
46
|
+
* that language, not a command sequence — a two-line ` ```ts ` block with two
|
|
47
|
+
* `export` statements is TypeScript, not shell, even though `export` is also
|
|
48
|
+
* a shell builtin (round-2 ruling item 2, found live: this task's own
|
|
49
|
+
* `brief-validation.ts` excerpt in `state-machine.md` reads exactly this
|
|
50
|
+
* way). A block outside this set never reaches the command-line count at
|
|
51
|
+
* all — it is not a candidate, regardless of its content.
|
|
52
|
+
*/
|
|
53
|
+
const SHELL_LANGS: ReadonlySet<string> = new Set(['', 'sh', 'bash', 'shell', 'console'])
|
|
54
|
+
|
|
55
|
+
function procedureLineNumberAt(content: string, index: number): number {
|
|
56
|
+
return content.slice(0, index).split('\n').length
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `aeg-root/templates/**` (or any adopter's `<doctrineRoot>/templates/**`) — worked-example templates that show the vendor-facing PR-report shape and legitimately carry a full command block. */
|
|
60
|
+
function isTemplatePath(path: string): boolean {
|
|
61
|
+
return /(^|\/)templates\//.test(path)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A fenced block containing two or more lines that each start with a shell
|
|
66
|
+
* command word is a command sequence, not an illustration — the failure
|
|
67
|
+
* names it: "this sequence is a `vinaya` command, name it." Exempt: a block
|
|
68
|
+
* inside the `AEG:VENDOR-EXAMPLE` anchor pair, and any file under a
|
|
69
|
+
* `templates/` directory.
|
|
70
|
+
*/
|
|
71
|
+
export function checkDoctrineNoProcedures(files: DoctrineFile[]): DoctrineProcedureFinding[] {
|
|
72
|
+
const findings: DoctrineProcedureFinding[] = []
|
|
73
|
+
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
if (isTemplatePath(file.path)) continue
|
|
76
|
+
const span = vendorExampleSpan(file.content)
|
|
77
|
+
|
|
78
|
+
for (const block of extractFencedBlocks(file.content)) {
|
|
79
|
+
if (span && block.start >= span.start && block.end <= span.end) continue
|
|
80
|
+
if (!SHELL_LANGS.has(block.lang)) continue
|
|
81
|
+
|
|
82
|
+
const commandLineCount = block.content.split('\n').filter(isCommandLine).length
|
|
83
|
+
if (commandLineCount >= 2) {
|
|
84
|
+
findings.push({
|
|
85
|
+
file: file.path,
|
|
86
|
+
line: procedureLineNumberAt(file.content, block.start),
|
|
87
|
+
message: `this fenced block has ${commandLineCount} shell-command lines — this sequence is a \`vinaya\` command, name it (or move it inside the \`AEG:VENDOR-EXAMPLE\` anchor / a \`templates/\` file).`
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return findings
|
|
94
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctrine portability — the class `check-reader-resolvable-prose` sweeps
|
|
3
|
+
* but cannot see (task 234, Issue #234). That check's own module header
|
|
4
|
+
* declares zero I/O, so it never resolves a cited path against a
|
|
5
|
+
* filesystem — and if it tried, it would resolve against the authoring
|
|
6
|
+
* repo, the one environment where every author-repo-internal path in
|
|
7
|
+
* `aeg-root/**` happens to exist.
|
|
8
|
+
*
|
|
9
|
+
* The reframe that makes this decidable with no I/O at all: the rule is not
|
|
10
|
+
* "this path does not resolve in the adopter" (which needs a filesystem to
|
|
11
|
+
* even ask) but **"portable doctrine names a non-portable path."** A cited
|
|
12
|
+
* path is judged purely by its own shape — its top path segment — against a
|
|
13
|
+
* fixed allow-list of prefixes known to be doctrine-relative or
|
|
14
|
+
* adopter-owned. Everything else is a finding, including a prefix nobody
|
|
15
|
+
* has classified yet: an allow-list fails closed on an unknown prefix (a
|
|
16
|
+
* build artifact is exactly as non-portable as the source it was built
|
|
17
|
+
* from), where a deny-list of "known author-repo prefixes" would fail open
|
|
18
|
+
* on the next unlisted one instead.
|
|
19
|
+
*
|
|
20
|
+
* **A second, additive dimension (Issue #298): a portable path is not the
|
|
21
|
+
* only way doctrine couples itself to one vendor.** Prose can name a
|
|
22
|
+
* specific AI company, product, or agent directly — "Claude Code", "GPT",
|
|
23
|
+
* "Anthropic" — with no path shape for the path-based predicate above to
|
|
24
|
+
* even look at. `checkDoctrinePortability` now emits a SECOND finding kind
|
|
25
|
+
* for exactly that: a fixed vendor-name word list, scanned against prose
|
|
26
|
+
* (never against code — a masked, `maskCode`-blind scan, the identical
|
|
27
|
+
* code-recognition grammar `anchored-region.ts` uses for the `AEG:*` PR/Issue
|
|
28
|
+
* body anchors, imported directly rather than re-implemented), with exactly
|
|
29
|
+
* ONE standing exemption: text inside a `<!-- AEG:VENDOR-EXAMPLE:START -->` /
|
|
30
|
+
* `<!-- AEG:VENDOR-EXAMPLE:END -->` pair — doctrine's one sanctioned, fenced
|
|
31
|
+
* home for naming today's shipped reference host by product name
|
|
32
|
+
* (`tranche-model.md` §12). A vendor word that is itself part of an
|
|
33
|
+
* already-portable path citation (`` `.claude/hooks/x.sh` ``, `` `CLAUDE.md` ``)
|
|
34
|
+
* is not re-flagged here: it is inline code, already masked by `maskCode`
|
|
35
|
+
* before the word scan ever runs, and the path-shape predicate above already
|
|
36
|
+
* has an opinion about it. This dimension has no I/O of its own either — same
|
|
37
|
+
* inputs, same adapter.
|
|
38
|
+
*
|
|
39
|
+
* Zero I/O: every input (file paths + contents) is read by the adapter and
|
|
40
|
+
* passed in.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { maskCode } from '@attalabs/aeg-forge-state/strip-code'
|
|
44
|
+
|
|
45
|
+
export type PortabilitySourceFile = { path: string; content: string }
|
|
46
|
+
|
|
47
|
+
export type PortabilityFinding = {
|
|
48
|
+
file: string
|
|
49
|
+
line: number
|
|
50
|
+
cited: string
|
|
51
|
+
message: string
|
|
52
|
+
/** `'path'` — the original non-portable-path predicate. `'vendor-name'` — Issue #298's word-list predicate. */
|
|
53
|
+
kind: 'path' | 'vendor-name'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** `aeg-root/**` by default — what this repo's own package ships and every adopter installs read-only. */
|
|
57
|
+
const DEFAULT_SHIPS_PREFIX = 'aeg-root/'
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Doctrine-relative (`roles/`, `contracts/`, `skills/`) and adopter-owned
|
|
61
|
+
* (`.github/`, `.vinaya/`, `.claude/`) top segments — the two portable
|
|
62
|
+
* classes measured in the task-234 corpus scan. `aeg-root/` itself is
|
|
63
|
+
* deliberately NOT a static entry here: it is always the CALLER-supplied
|
|
64
|
+
* `shipsPrefix`, checked dynamically in `isPortable` below, so an adopter
|
|
65
|
+
* who configures a non-default `doctrineRoot` gets that root treated as
|
|
66
|
+
* portable too — a static `'aeg-root/'` entry would falsely flag every one
|
|
67
|
+
* of that adopter's own self-citations as non-portable. A citation whose
|
|
68
|
+
* top segment falls outside this list (and isn't the ships prefix) is
|
|
69
|
+
* never assumed portable, however plausible it looks; it is a finding, and
|
|
70
|
+
* someone extends this list deliberately once its side of the
|
|
71
|
+
* classification is decided.
|
|
72
|
+
*/
|
|
73
|
+
const STATIC_PORTABLE_PREFIXES: readonly string[] = [
|
|
74
|
+
'roles/',
|
|
75
|
+
'contracts/',
|
|
76
|
+
'skills/',
|
|
77
|
+
'.github/',
|
|
78
|
+
'.vinaya/',
|
|
79
|
+
'.claude/',
|
|
80
|
+
// Not in the task-234 corpus-scan table, but verified while reading the
|
|
81
|
+
// corpus: `.git/hooks/*` and `.husky/*` are cited only as the pre-commit/
|
|
82
|
+
// pre-push hook locations every git repo (and every husky-adopting repo)
|
|
83
|
+
// genuinely has — adopter-owned by construction, the same class as
|
|
84
|
+
// `.github/`/`.vinaya/`/`.claude/` above.
|
|
85
|
+
'.git/',
|
|
86
|
+
'.husky/'
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The three illustrative placeholders measured in the corpus — a
|
|
91
|
+
* `[path/inside/the/...]`-shaped fill-in-the-blank in a template, never a
|
|
92
|
+
* real citation. Exempted by literal, not by pattern, so a real path that
|
|
93
|
+
* happens to share a prefix with one of these is never accidentally waved
|
|
94
|
+
* through.
|
|
95
|
+
*/
|
|
96
|
+
const EXEMPT_LITERALS: ReadonlySet<string> = new Set([
|
|
97
|
+
'path/inside/the/shipped/diff.ts',
|
|
98
|
+
'path/inside/the/surface.ts',
|
|
99
|
+
'apps/x/specs/...'
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A cited path, inline-backtick-delimited, shaped like a repo-relative
|
|
104
|
+
* path: starts with a word/dot/dash character (never `/` — that shape is a
|
|
105
|
+
* web route, e.g. `` `/docs/state-machine` ``, not a repo path), contains at
|
|
106
|
+
* least one `/`, and carries no character a URL or a prose fragment would
|
|
107
|
+
* (no `:`, no whitespace, no parens) — which also excludes a link like
|
|
108
|
+
* `` `https://vinaya.dev` `` without a separate URL-shaped exclusion.
|
|
109
|
+
*/
|
|
110
|
+
const CITED_PATH_PATTERN = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]*)+$/
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Top segments that are never a repo path, verified against every one of
|
|
114
|
+
* their occurrences in the real corpus rather than assumed: `origin/`/`refs/`
|
|
115
|
+
* are git-ref namespace (`origin/main`, `refs/pull/N/merge`), and `vinaya/`/
|
|
116
|
+
* `fix/` are this doctrine's own GitHub-label and branch-name-example
|
|
117
|
+
* conventions (`vinaya/blocked`, `fix/brief-gate-nontask`) — a citation kind
|
|
118
|
+
* this check does not judge at all, portable or not, the same way it never
|
|
119
|
+
* tries to also judge a forge number or a tranche slug.
|
|
120
|
+
*
|
|
121
|
+
* **A closed, tested set, not a growable escape hatch.** Excluding a top
|
|
122
|
+
* segment here means every citation under it is invisible to this check —
|
|
123
|
+
* never flagged, however non-portable it would otherwise be — so this set
|
|
124
|
+
* carries exactly the corpus-verified exceptions above and nothing else.
|
|
125
|
+
* `doctrine-portability.test.ts` locks its exact membership; an addition
|
|
126
|
+
* that doesn't also update that lock is a bug, not a silent expansion.
|
|
127
|
+
* Exported for that lock, not for callers to extend at runtime.
|
|
128
|
+
*/
|
|
129
|
+
export const NON_PATH_TOP_SEGMENTS: ReadonlySet<string> = new Set(['origin', 'refs', 'HEAD', 'vinaya', 'fix'])
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The vendor-name word list (Issue #298's re-count, live at authoring):
|
|
133
|
+
* every AI company/product/agent name this doctrine's own prose has
|
|
134
|
+
* actually used, as either the shipped reference host or a peer example —
|
|
135
|
+
* a real, hardcoded list for this repo's own corpus, not a growable
|
|
136
|
+
* config surface (brief `fix/doctrine-vendor-neutrality` §10: ship a
|
|
137
|
+
* reasonable hardcoded list, flag generalization for later). Longest
|
|
138
|
+
* alternative first ("claude code" before "claude") so a two-word mention
|
|
139
|
+
* is reported whole rather than as a truncated single-word match followed
|
|
140
|
+
* by a dangling "code".
|
|
141
|
+
*
|
|
142
|
+
* Includes model-TIER names (`opus`/`sonnet`/`haiku`), not just
|
|
143
|
+
* company/product names — a review round on this task's own PR (#338)
|
|
144
|
+
* found the first cut missed exactly this class: `brief-authoring/SKILL.md`
|
|
145
|
+
* and `brief-template.md` named a specific model tier bare, in prose, in
|
|
146
|
+
* three places the path-shape predicate could never see either. `sonnet`
|
|
147
|
+
* and `haiku` are ordinary English words outside this domain — a real
|
|
148
|
+
* false-positive risk for a general-purpose tool, accepted here on the
|
|
149
|
+
* same "reasonable hardcoded list for this repo's own corpus" basis as the
|
|
150
|
+
* rest of this list (verified against the live corpus: no non-vendor use
|
|
151
|
+
* of either word exists in `aeg-root/**` today).
|
|
152
|
+
*/
|
|
153
|
+
const VENDOR_NAME_SOURCE =
|
|
154
|
+
'\\bclaude code\\b|\\bclaude\\b|\\banthropic\\b|\\bchatgpt\\b|\\bopenai\\b|\\bgpt\\b|\\bgemini\\b|\\bcodex\\b|\\bgrok\\b|\\bdeepseek\\b|\\bopus\\b|\\bsonnet\\b|\\bhaiku\\b'
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Exported (alongside `VENDOR_EXAMPLE_END` below) so `doctrine-no-procedures.ts`
|
|
158
|
+
* can find the same fenced home's raw-text span — never a second copy of this
|
|
159
|
+
* pattern — to exempt a fenced block that sits inside it, the same way this
|
|
160
|
+
* file exempts the region from its own vendor-name word scan.
|
|
161
|
+
*/
|
|
162
|
+
export const VENDOR_EXAMPLE_START = /<!--\s*AEG:VENDOR-EXAMPLE:START\s*-->/
|
|
163
|
+
export const VENDOR_EXAMPLE_END = /<!--\s*AEG:VENDOR-EXAMPLE:END\s*-->/
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Blanks (same-length, index-preserving — same discipline as `maskCode`
|
|
167
|
+
* itself) the region between the first well-formed
|
|
168
|
+
* `<!-- AEG:VENDOR-EXAMPLE:START -->` … `<!-- AEG:VENDOR-EXAMPLE:END -->`
|
|
169
|
+
* pair in `masked` — doctrine's one sanctioned home for naming today's
|
|
170
|
+
* shipped reference host by product name (`tranche-model.md` §12).
|
|
171
|
+
* Markers are searched on already-`maskCode`d text, the same order
|
|
172
|
+
* `anchoredRegionBounds` searches the `AEG:*` PR/Issue-body anchors, so a
|
|
173
|
+
* decoy pair quoted inside a fenced example never wins. A START with no
|
|
174
|
+
* following END is not a fence at all — the same "malformed half-pair is no
|
|
175
|
+
* anchor" rule `anchored-region.ts` applies.
|
|
176
|
+
*
|
|
177
|
+
* **First pair wins, same as `anchoredRegionBounds` — a SECOND pair in the
|
|
178
|
+
* same file is not masked.** By design there is exactly one fenced home in
|
|
179
|
+
* the whole doctrine tree (the Goal this check exists to hold), so a
|
|
180
|
+
* second pair anywhere is itself a doctrine defect, not a shape this
|
|
181
|
+
* function needs to accommodate; scanning per-file rather than per-pair
|
|
182
|
+
* keeps that failure visible (a second, unmasked pair still reports its
|
|
183
|
+
* own `vendor-name` findings) instead of silently exempting it too.
|
|
184
|
+
*/
|
|
185
|
+
function maskVendorExampleRegion(masked: string): string {
|
|
186
|
+
const start = VENDOR_EXAMPLE_START.exec(masked)
|
|
187
|
+
if (!start) return masked
|
|
188
|
+
const afterStart = start.index + start[0].length
|
|
189
|
+
const end = VENDOR_EXAMPLE_END.exec(masked.slice(afterStart))
|
|
190
|
+
if (!end) return masked
|
|
191
|
+
const regionEnd = afterStart + end.index + end[0].length
|
|
192
|
+
const region = masked.slice(start.index, regionEnd)
|
|
193
|
+
const blanked = region.replace(/[^\n]/g, ' ')
|
|
194
|
+
return masked.slice(0, start.index) + blanked + masked.slice(regionEnd)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Every vendor-name-list match in `content`, code-blind (`maskCode`, the
|
|
199
|
+
* same grammar `anchored-region.ts` uses) and blind to the one fenced
|
|
200
|
+
* `AEG:VENDOR-EXAMPLE` home. A vendor word that is only part of an
|
|
201
|
+
* already-portable inline-code path citation (`` `.claude/hooks/x.sh` ``,
|
|
202
|
+
* `` `CLAUDE.md` ``) never reaches this scan at all — it is masked before
|
|
203
|
+
* the word list ever runs, the same way a fenced worked example is blind to
|
|
204
|
+
* `body-bare-digits`.
|
|
205
|
+
*/
|
|
206
|
+
function extractVendorMentions(content: string): { name: string; index: number }[] {
|
|
207
|
+
const scoped = maskVendorExampleRegion(maskCode(content))
|
|
208
|
+
const found: { name: string; index: number }[] = []
|
|
209
|
+
const pattern = new RegExp(VENDOR_NAME_SOURCE, 'gi')
|
|
210
|
+
let match: RegExpExecArray | null = pattern.exec(scoped)
|
|
211
|
+
while (match !== null) {
|
|
212
|
+
found.push({ name: match[0], index: match.index })
|
|
213
|
+
match = pattern.exec(scoped)
|
|
214
|
+
}
|
|
215
|
+
return found
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Every inline-backtick span in `content`, tested against `CITED_PATH_PATTERN`. */
|
|
219
|
+
function extractCitedPaths(content: string): { cited: string; index: number }[] {
|
|
220
|
+
const found: { cited: string; index: number }[] = []
|
|
221
|
+
const spanPattern = /`([^`\n]+)`/g
|
|
222
|
+
let match: RegExpExecArray | null = spanPattern.exec(content)
|
|
223
|
+
while (match !== null) {
|
|
224
|
+
const cited = match[1] ?? ''
|
|
225
|
+
const topSegment = cited.slice(0, cited.indexOf('/'))
|
|
226
|
+
if (CITED_PATH_PATTERN.test(cited) && !NON_PATH_TOP_SEGMENTS.has(topSegment)) {
|
|
227
|
+
found.push({ cited, index: match.index })
|
|
228
|
+
}
|
|
229
|
+
match = spanPattern.exec(content)
|
|
230
|
+
}
|
|
231
|
+
return found
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function lineAtIndex(content: string, index: number): number {
|
|
235
|
+
let line = 1
|
|
236
|
+
for (let i = 0; i < index; i++) {
|
|
237
|
+
if (content.charCodeAt(i) === 10) line++
|
|
238
|
+
}
|
|
239
|
+
return line
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* True iff `cited`'s top segment is portable: the caller-supplied
|
|
244
|
+
* `shipsPrefix` itself (doctrine citing its own tree, wherever that tree
|
|
245
|
+
* actually lives for this caller), or one of the static portable prefixes.
|
|
246
|
+
*/
|
|
247
|
+
function isPortable(cited: string, shipsPrefix: string): boolean {
|
|
248
|
+
if (cited.startsWith(shipsPrefix)) return true
|
|
249
|
+
return STATIC_PORTABLE_PREFIXES.some((prefix) => cited.startsWith(prefix))
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Sweeps every file under `shipsPrefix` for two independent finding kinds:
|
|
254
|
+
* a cited path whose top segment is not allow-listed portable (`'path'`),
|
|
255
|
+
* and a vendor-name-list word used in prose outside the one fenced
|
|
256
|
+
* `AEG:VENDOR-EXAMPLE` home (`'vendor-name'`, Issue #298). Files outside
|
|
257
|
+
* `shipsPrefix` are out of scope entirely — this check only judges what the
|
|
258
|
+
* shipped doctrine tree itself cites/names, never a repo's other source.
|
|
259
|
+
*/
|
|
260
|
+
export function checkDoctrinePortability(
|
|
261
|
+
files: readonly PortabilitySourceFile[],
|
|
262
|
+
shipsPrefix: string = DEFAULT_SHIPS_PREFIX
|
|
263
|
+
): PortabilityFinding[] {
|
|
264
|
+
const findings: PortabilityFinding[] = []
|
|
265
|
+
|
|
266
|
+
for (const file of files) {
|
|
267
|
+
if (!file.path.startsWith(shipsPrefix)) continue
|
|
268
|
+
|
|
269
|
+
for (const { cited, index } of extractCitedPaths(file.content)) {
|
|
270
|
+
if (EXEMPT_LITERALS.has(cited)) continue
|
|
271
|
+
if (isPortable(cited, shipsPrefix)) continue
|
|
272
|
+
findings.push({
|
|
273
|
+
file: file.path,
|
|
274
|
+
line: lineAtIndex(file.content, index),
|
|
275
|
+
cited,
|
|
276
|
+
message: `cites "${cited}", a path that only exists in the authoring repository — not portable doctrine`,
|
|
277
|
+
kind: 'path'
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for (const { name, index } of extractVendorMentions(file.content)) {
|
|
282
|
+
findings.push({
|
|
283
|
+
file: file.path,
|
|
284
|
+
line: lineAtIndex(file.content, index),
|
|
285
|
+
cited: name,
|
|
286
|
+
message: `names "${name}" outside the one fenced vendor-example home (an <!-- AEG:VENDOR-EXAMPLE:START --> … <!-- AEG:VENDOR-EXAMPLE:END --> pair) — portable doctrine refers to a host generically everywhere else`,
|
|
287
|
+
kind: 'vendor-name'
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return findings
|
|
293
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seam the pure derivation (`deriveDiagramModel`) consumes doctrine
|
|
3
|
+
* through, instead of reading `aeg-root/` paths directly. Doctrine is the raw
|
|
4
|
+
* markdown that governs the methodology — `enforcement.md`, the role files,
|
|
5
|
+
* the contract files. Implementations (file-backed today, package-bundled for
|
|
6
|
+
* adopters tomorrow) perform I/O and therefore live outside `aeg-core`
|
|
7
|
+
* (`apps/vinaya/sources`) — this package only defines the contract, so the
|
|
8
|
+
* library can be packaged for repos that have no `aeg-root/` of their own
|
|
9
|
+
*. Same discipline as `state-source.ts`: zero I/O here, async on the
|
|
10
|
+
* adapter so callers get one uniform type regardless of the backing store.
|
|
11
|
+
*/
|
|
12
|
+
export type DoctrineContent = {
|
|
13
|
+
/** Raw `enforcement.md` content. */
|
|
14
|
+
enforcement: string
|
|
15
|
+
/** Raw `roles/*.md` files (path + content). */
|
|
16
|
+
roles: Array<{ path: string; content: string }>
|
|
17
|
+
/** Raw `contracts/*.md` files (path + content). */
|
|
18
|
+
contracts: Array<{ path: string; content: string }>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type DoctrineSource = {
|
|
22
|
+
getDoctrine(): Promise<DoctrineContent>
|
|
23
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotent label-minting (vinaya-label-hygiene-v1 task 1, #243) — the
|
|
3
|
+
* decision logic three call sites (`apps/cli/src/commands/audit.ts`,
|
|
4
|
+
* `bin/check-direct-main-push.ts`, `bin/dead-branch-audit.ts`) each
|
|
5
|
+
* reimplemented: does this label already exist on the forge, and if not,
|
|
6
|
+
* create it. Pure — no `gh` shell-outs here; each call site injects its own
|
|
7
|
+
* `listLabelNames`/`createLabel`, so this module stays agnostic to how a
|
|
8
|
+
* caller shells out (array-arg `execFileSync` vs. string `execSync`) and to
|
|
9
|
+
* whether a caller lets `gh` failures throw or swallows them — those are
|
|
10
|
+
* call-site error-handling policies this extraction does not change.
|
|
11
|
+
*
|
|
12
|
+
* Mints lazily, on first use, by design — never called to bootstrap the
|
|
13
|
+
* full label vocabulary (`@attalabs/aeg-forge-state`'s `labels.ts`) ahead of
|
|
14
|
+
* need.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export type LabelExistenceIo = {
|
|
18
|
+
listLabelNames: (repoFlag: string) => string[]
|
|
19
|
+
createLabel: (repoFlag: string, name: string, description: string, color: string) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const LABEL_COLOR = 'B60205'
|
|
23
|
+
|
|
24
|
+
export function ensureLabelExists(repoFlag: string, name: string, description: string, io: LabelExistenceIo): void {
|
|
25
|
+
const existing = io.listLabelNames(repoFlag)
|
|
26
|
+
if (existing.includes(name)) return
|
|
27
|
+
io.createLabel(repoFlag, name, description, LABEL_COLOR)
|
|
28
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path classifiers shared by the tier-derivation and doc-owners checks.
|
|
3
|
+
* Pure string predicates — no I/O.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Decision archives: frozen records of what was decided, not documentation.
|
|
8
|
+
* Touching one must never satisfy C3's code-requires-docs pairing, carry a
|
|
9
|
+
* tier signal, or be held to a spec's `Status:` block.
|
|
10
|
+
*
|
|
11
|
+
* Recognized by filename suffix, never by an enumerated path list. An earlier
|
|
12
|
+
* form of this predicate hardcoded each consumer's archive by full path, which
|
|
13
|
+
* put specific product names inside a package that ships to adopters who have
|
|
14
|
+
* neither. The suffix is the actual rule — a file named `…decisions-legacy.md`
|
|
15
|
+
* is an archive in any repo — and it subsumes every path that list held, so
|
|
16
|
+
* the coupling was removable with no behavior change.
|
|
17
|
+
*
|
|
18
|
+
* The historical failure the path list was added to prevent (renaming an
|
|
19
|
+
* archive out of recognition) is not re-opened: the earlier rule required a
|
|
20
|
+
* `-decisions-legacy.md` suffix and so missed a top-level `decisions-legacy.md`
|
|
21
|
+
* with no prefix, which is why the list existed at all. Matching the bare
|
|
22
|
+
* suffix covers both shapes.
|
|
23
|
+
*/
|
|
24
|
+
export function isFrozenArchive(p: string): boolean {
|
|
25
|
+
return p.endsWith('decisions-legacy.md')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isDocFile(p: string): boolean {
|
|
29
|
+
return (
|
|
30
|
+
(p.startsWith('aeg-root/') && p.endsWith('.md')) ||
|
|
31
|
+
(p.startsWith('aeg-project/') && p.endsWith('.md')) ||
|
|
32
|
+
(p.includes('/aeg-project/') && p.endsWith('.md')) ||
|
|
33
|
+
(p.startsWith('.vinaya/') && p.endsWith('.md')) ||
|
|
34
|
+
(p.startsWith('docs/') && p.endsWith('.md') && !isFrozenArchive(p)) ||
|
|
35
|
+
(p.startsWith('apps/') && p.includes('/specs/') && p.endsWith('.md') && !isFrozenArchive(p)) ||
|
|
36
|
+
(p.startsWith('.claude/skills/') && p.endsWith('.md')) ||
|
|
37
|
+
/^(apps|packages)\/[^/]+\/README\.md$/.test(p) ||
|
|
38
|
+
p === 'docs-index.md' ||
|
|
39
|
+
p === 'README.md' ||
|
|
40
|
+
p === 'CLAUDE.md'
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isCodeFile(p: string): boolean {
|
|
45
|
+
if (p.endsWith('.md')) return false
|
|
46
|
+
return /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|sql|css)$/.test(p)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isSpecFile(p: string): boolean {
|
|
50
|
+
return p.startsWith('apps/') && p.includes('/specs/') && p.endsWith('.md') && !isFrozenArchive(p)
|
|
51
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-push dispatch gate (aeg-governance-hardening task 25, #365). Pure —
|
|
3
|
+
* no `fs`, no `gh`/`git` shell-outs. The CLI shim (`bin/check-first-push-
|
|
4
|
+
* dispatch.ts`, wired into `.husky/pre-push`) runs the UNCHANGED
|
|
5
|
+
* `verify-dispatch.ts` gate mode once on a `task/<tranche>/<n>` branch's
|
|
6
|
+
* first push, maps its exit/output to a `DispatchReadinessFact`, and passes
|
|
7
|
+
* it in here.
|
|
8
|
+
*
|
|
9
|
+
* Mechanizes `roles/developer.md`'s entry-gate items 3–7 at the earliest
|
|
10
|
+
* possible moment — the first push — instead of relying on the Developer to
|
|
11
|
+
* run `verify-dispatch.ts` by hand before Step 0. Subsequent pushes (once a
|
|
12
|
+
* PR exists) skip the gate: dispatch readiness was validated once, and
|
|
13
|
+
* re-blocking mid-task on a sibling's later state change would strand
|
|
14
|
+
* in-flight work.
|
|
15
|
+
*
|
|
16
|
+
* `UNKNOWN` (forge unreachable — `verify-dispatch.ts`'s own `severity:infra`
|
|
17
|
+
* marker, printed when repo/token resolution fails) maps to `allow`, the
|
|
18
|
+
* same deliberate fail-open choice `dead-branch-push-guard.ts` already makes
|
|
19
|
+
* for the sibling gate: a guard that can block *all* pushes on a
|
|
20
|
+
* transient forge-reachability issue is worse than the bug it fixes. See
|
|
21
|
+
* `aeg-root/enforcement.md`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** A `task/<tranche>/<n>` branch parses to its two topology coordinates; anything else does not. */
|
|
25
|
+
export function parseTaskBranch(branch: string): { tranche: string; taskId: string } | null {
|
|
26
|
+
const m = /^task\/([^/]+)\/([^/]+)$/.exec(branch)
|
|
27
|
+
return m ? { tranche: m[1] as string, taskId: m[2] as string } : null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type DispatchReadinessFact = 'READY' | 'NOT_READY' | 'UNKNOWN'
|
|
31
|
+
|
|
32
|
+
export type FirstPushDispatchGateInput = {
|
|
33
|
+
/** The branch being pushed. */
|
|
34
|
+
branch: string
|
|
35
|
+
/** Whether a PR already exists for this branch — the hook's existing C5/ detection, reused, never re-derived here. */
|
|
36
|
+
prExists: boolean
|
|
37
|
+
/** verify-dispatch's classified result. Irrelevant (never read) when the branch is not a task branch or a PR already exists. */
|
|
38
|
+
readiness: DispatchReadinessFact
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type FirstPushDispatchGateVerdict = 'allow' | 'refuse'
|
|
42
|
+
|
|
43
|
+
export type FirstPushDispatchGateResult = { verdict: FirstPushDispatchGateVerdict; reason: string }
|
|
44
|
+
|
|
45
|
+
export function checkFirstPushDispatchGate(input: FirstPushDispatchGateInput): FirstPushDispatchGateResult {
|
|
46
|
+
const { branch, prExists, readiness } = input
|
|
47
|
+
|
|
48
|
+
if (parseTaskBranch(branch) === null) {
|
|
49
|
+
return {
|
|
50
|
+
verdict: 'allow',
|
|
51
|
+
reason: `Branch \`${branch}\` is not a task/<tranche>/<n> branch — the first-push dispatch gate only applies to task branches.`
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (prExists) {
|
|
56
|
+
return {
|
|
57
|
+
verdict: 'allow',
|
|
58
|
+
reason: `Branch \`${branch}\` already has an open PR — dispatch readiness was already validated on its first push; later pushes are not re-blocked by a sibling task's state change.`
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (readiness === 'UNKNOWN') {
|
|
63
|
+
return {
|
|
64
|
+
verdict: 'allow',
|
|
65
|
+
reason: `verify-dispatch could not reach the forge for \`${branch}\` (repo/token unresolvable) — failing OPEN rather than blocking the push on a transient issue.`
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (readiness === 'NOT_READY') {
|
|
70
|
+
return {
|
|
71
|
+
verdict: 'refuse',
|
|
72
|
+
reason: `verify-dispatch reports NOT READY for \`${branch}\` — see the failing predicate printed above.`
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
verdict: 'allow',
|
|
78
|
+
reason: `verify-dispatch reports READY TO DISPATCH for \`${branch}\`.`
|
|
79
|
+
}
|
|
80
|
+
}
|