@attalabs/vinaya 0.22.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 +13 -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 +552 -139
- package/dist/checks/bin/check-branch-topology.js +603 -144
- package/dist/checks/bin/check-brief-shape.js +593 -142
- package/dist/checks/bin/check-changeset-coverage.js +935 -161
- package/dist/checks/bin/check-closes-n.js +603 -144
- package/dist/checks/bin/check-coherence.js +614 -147
- package/dist/checks/bin/check-dead-branch-push.js +552 -139
- package/dist/checks/bin/check-dispatch-readiness.js +612 -145
- package/dist/checks/bin/check-doc-coverage-push.js +689 -218
- package/dist/checks/bin/check-doc-coverage.js +689 -218
- package/dist/checks/bin/check-doctrine-no-procedures.js +4794 -0
- package/dist/checks/bin/check-doctrine-portability.js +684 -146
- package/dist/checks/bin/check-evidence-fresh.js +994 -174
- package/dist/checks/bin/check-exec-bits.js +4931 -0
- package/dist/checks/bin/check-first-push-dispatch.js +603 -144
- package/dist/checks/bin/check-issue-assignment.js +603 -144
- package/dist/checks/bin/check-main-branch-refusal.js +552 -139
- package/dist/checks/bin/check-no-disk-state.js +552 -139
- package/dist/checks/bin/check-pr-report-density.js +4461 -0
- package/dist/checks/bin/check-quoted-command.js +621 -155
- package/dist/checks/bin/check-reader-resolvable-prose.js +617 -154
- package/dist/checks/bin/check-registry-gates.js +595 -144
- package/dist/checks/bin/check-retired-vocabulary.js +617 -154
- package/dist/checks/bin/check-review-gate.js +670 -141
- package/dist/checks/bin/check-single-plan-pr.js +552 -139
- package/dist/checks/bin/check-test-plan.js +553 -140
- package/dist/checks/bin/check-token-collection-wired.js +552 -139
- package/dist/checks/bin/check-token-report.js +552 -139
- package/dist/checks/bin/check-workspace-escape.js +985 -145
- package/dist/index.js +8500 -5518
- 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]__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]__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/_1fqw88f._.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/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 → 0jer_drkl1e4r.js} +4 -4
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{267o-tsrnuwnv.js → 20x-t0a0v129z.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1hufpuody6vjv.js → 2ou_81ahqw31e.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/aeg-core}/bin/verify-coherence.ts +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C7 — the published-prose check. Pure and zero-I/O, shaped like
|
|
3
|
+
* `docs-coherence.ts` (parsed entries plus the model-backed surfaced-path
|
|
4
|
+
* allowlist in, `{ errors, notes }` out).
|
|
5
|
+
*
|
|
6
|
+
* A role or contract doc is read by two audiences with opposite needs: an
|
|
7
|
+
* agent taking the role reads it end to end as its operating instruction, and
|
|
8
|
+
* a person landing on `/docs/roles/<id>` reads the same file cold. The answer
|
|
9
|
+
* is one short, binding section at the top of every role and contract — `##
|
|
10
|
+
* The short version` — which is what publishes; everything under `##
|
|
11
|
+
* Reference` explains it and stays in the repo. This check enforces two
|
|
12
|
+
* properties of that section, over the published set only:
|
|
13
|
+
*
|
|
14
|
+
* 1. **Structure** — every surfaced `roles/*.md` and `contracts/*.md` opens
|
|
15
|
+
* with a `## The short version` section carrying all four bold-led
|
|
16
|
+
* blocks, in order, inside the word band. The blocks answer the same four
|
|
17
|
+
* questions for a role and for a seam (what it owns/carries, when it
|
|
18
|
+
* refuses, what it never does, how it physically runs), which is why the
|
|
19
|
+
* lead-ins differ by kind but the count and order never do.
|
|
20
|
+
* 2. **Readability** — the text that actually reaches a reader carries no
|
|
21
|
+
* token a stranger cannot resolve from the page: no decision id, no
|
|
22
|
+
* section sign, no forge number, no retired public name, no tranche
|
|
23
|
+
* slug, no repo-internal path, and no label vocabulary (namespaced or
|
|
24
|
+
* retired alike — a published page should name neither, and flagging only
|
|
25
|
+
* the current grammar would leave every retired mention invisible).
|
|
26
|
+
*
|
|
27
|
+
* **What "published" means here is deliberately narrow.** For a role or
|
|
28
|
+
* contract it is the short version, not the file. For `enforcement.md` it is
|
|
29
|
+
* the introduction plus the four columns the page renders — the row's own
|
|
30
|
+
* name, `Summary`, `Category`, `Description`. The enforcing columns (`Gate`,
|
|
31
|
+
* `What must be true…`, `Re-verifies`, `Runs`, `Catches`, `implementation`,
|
|
32
|
+
* `lock`) are never scanned: they never reach a page, and flagging them would
|
|
33
|
+
* demand rewriting the gate registry to suit a website. Columns resolve by
|
|
34
|
+
* HEADER NAME, never by index — the three ring tables have different shapes,
|
|
35
|
+
* and `Summary`/`Category`/`Description` are the ones spelled identically in
|
|
36
|
+
* all three, the same property `registry-parse.ts` relies on for
|
|
37
|
+
* `Description`.
|
|
38
|
+
*
|
|
39
|
+
* **Protocol mechanics are published on purpose.** `.worktrees/task/<tranche>/<n>`
|
|
40
|
+
* and the `task/<tranche>/<n>` branch convention are the method an adopter
|
|
41
|
+
* must learn, not this repo's internals — they are masked out before scanning
|
|
42
|
+
* so a path rule can never eat them, and that exemption is tested rather than
|
|
43
|
+
* incidental.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { findHeadingLine, findTable } from '../markdown-table'
|
|
47
|
+
import { isSurfacedDoc } from './surfaced-manifest'
|
|
48
|
+
import type { DocFrontmatter } from './types'
|
|
49
|
+
|
|
50
|
+
export const SHORT_VERSION_HEADING = 'The short version'
|
|
51
|
+
export const REFERENCE_HEADING = 'Reference'
|
|
52
|
+
|
|
53
|
+
/** Words. Under the floor means something was dropped; over the ceiling means
|
|
54
|
+
* the reference is being summarised instead of the rule being stated. */
|
|
55
|
+
export const SHORT_VERSION_MIN_WORDS = 150
|
|
56
|
+
export const SHORT_VERSION_MAX_WORDS = 450
|
|
57
|
+
|
|
58
|
+
/** The four blocks, in order, as a role answers them. */
|
|
59
|
+
export const ROLE_BLOCKS = ['You own', 'You refuse', 'You never', 'How it physically runs'] as const
|
|
60
|
+
|
|
61
|
+
/** The same four blocks, as a seam answers them. */
|
|
62
|
+
export const CONTRACT_BLOCKS = [
|
|
63
|
+
'What crosses',
|
|
64
|
+
'The hand-off is malformed',
|
|
65
|
+
'What it does not carry',
|
|
66
|
+
'How it physically runs'
|
|
67
|
+
] as const
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Protocol facts that must survive every readability rule. Masked out of the
|
|
71
|
+
* text before scanning, longest first, so a repo-internal-path rule can never
|
|
72
|
+
* claim the branch convention.
|
|
73
|
+
*/
|
|
74
|
+
export const ALLOWED_MECHANICS: readonly string[] = [
|
|
75
|
+
'.worktrees/task/<tranche>/<n>',
|
|
76
|
+
'task/<tranche>/<n>',
|
|
77
|
+
'.worktrees/'
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
export type PublishedProseEntry = {
|
|
81
|
+
/** Path relative to `aeg-root/`, e.g. `roles/developer.md`. */
|
|
82
|
+
relPath: string
|
|
83
|
+
frontmatter: Pick<DocFrontmatter, 'surfaced' | 'title' | 'description'>
|
|
84
|
+
body: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type PublishedProseResult = { errors: string[]; notes: string[] }
|
|
88
|
+
|
|
89
|
+
type ProseRule = { id: string; what: string; pattern: RegExp }
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Every rule is case-sensitive on purpose where the token itself is: forge
|
|
93
|
+
* labels are lower-case, so a lower-case-only match keeps a capitalized
|
|
94
|
+
* English word ("Project:", the PR-body field) from being read as the deleted
|
|
95
|
+
* `project:*` label family.
|
|
96
|
+
*/
|
|
97
|
+
const PROSE_RULES: ProseRule[] = [
|
|
98
|
+
{ id: 'decision-id', what: 'a decision id', pattern: /\bD-\d{3,}\b/ },
|
|
99
|
+
{ id: 'section-ref', what: 'a section reference', pattern: /§/ },
|
|
100
|
+
{ id: 'forge-number', what: 'a forge number', pattern: /#\d+/ },
|
|
101
|
+
{ id: 'retired-name', what: 'the retired public name', pattern: /\bAEG\b/ },
|
|
102
|
+
{ id: 'tranche-slug', what: 'a tranche slug', pattern: /\b[a-z][a-z0-9]*(?:-[a-z0-9]+)+-v\d+\b/ },
|
|
103
|
+
{
|
|
104
|
+
id: 'label-vocabulary',
|
|
105
|
+
what: 'forge label vocabulary',
|
|
106
|
+
// Every entry here is CUMULATIVE — a retired namespace stays banned after
|
|
107
|
+
// the objects carrying it are gone, because a published page naming it is
|
|
108
|
+
// just as unreadable to a stranger as one naming the current grammar (see
|
|
109
|
+
// the module header). `aeg:` is retired and still listed for that reason,
|
|
110
|
+
// and `iteration:` joins it: the rename to `tranche:` ADDS an alternative,
|
|
111
|
+
// it does not substitute one. Substituting is how a rename silently
|
|
112
|
+
// narrows this gate — the forge still carried `vinaya/iteration:*` labels
|
|
113
|
+
// when the rename landed, and a page naming one would have passed.
|
|
114
|
+
pattern: /(?:vinaya\/)?\b(?:tranche|iteration|tier|needs|waiver|project|aeg):(?![/\s]|$)/
|
|
115
|
+
},
|
|
116
|
+
{ id: 'internal-path', what: 'a repo-internal path', pattern: /\b(?:packages|apps|aeg-root|tools|scripts)\// },
|
|
117
|
+
{ id: 'internal-path', what: 'a repo-internal path', pattern: /\.(?:claude|husky|github)\// },
|
|
118
|
+
{
|
|
119
|
+
id: 'internal-path',
|
|
120
|
+
what: 'a repo-internal file',
|
|
121
|
+
pattern: /(?:^|[\s(`"'[])[\w.-]+(?:\/[\w.-]+)*\.(?:ts|tsx|js|jsx|mjs|cjs|sh|ya?ml|json|md)\b/
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
/** The doc kinds this check governs, and the block set each answers with. */
|
|
126
|
+
function blocksFor(relPath: string): readonly string[] | null {
|
|
127
|
+
if (relPath.startsWith('roles/')) return ROLE_BLOCKS
|
|
128
|
+
if (relPath.startsWith('contracts/')) return CONTRACT_BLOCKS
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function sectionBody(body: string, heading: string): string | null {
|
|
133
|
+
const lines = body.split('\n')
|
|
134
|
+
const start = lines.findIndex((line) => line.trim() === `## ${heading}`)
|
|
135
|
+
if (start === -1) return null
|
|
136
|
+
const rest = lines.slice(start + 1)
|
|
137
|
+
const end = rest.findIndex((line) => /^##\s/.test(line))
|
|
138
|
+
return (end === -1 ? rest : rest.slice(0, end)).join('\n').trim()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The section a role/contract page publishes, or null when the doc carries
|
|
143
|
+
* none. The single extraction both the gate and Vinaya's `/docs` renderer
|
|
144
|
+
* read, so the page and the check can never disagree about what "published"
|
|
145
|
+
* means.
|
|
146
|
+
*/
|
|
147
|
+
export function extractShortVersion(body: string): string | null {
|
|
148
|
+
return sectionBody(body, SHORT_VERSION_HEADING)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* What a role/contract page renders, with a degradation ladder that never
|
|
153
|
+
* leaks the reference: the short version when present; otherwise the body
|
|
154
|
+
* above `## Reference`; otherwise the whole body (a doc with neither heading
|
|
155
|
+
* is not governed by C7's structure rule and has no reference to hide).
|
|
156
|
+
*/
|
|
157
|
+
export function publishedDoctrineBody(body: string): string {
|
|
158
|
+
const short = extractShortVersion(body)
|
|
159
|
+
if (short !== null) return short
|
|
160
|
+
const lines = body.split('\n')
|
|
161
|
+
const referenceAt = lines.findIndex((line) => line.trim() === `## ${REFERENCE_HEADING}`)
|
|
162
|
+
return referenceAt === -1 ? body : lines.slice(0, referenceAt).join('\n').trim()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function countWords(text: string): number {
|
|
166
|
+
return text.split(/\s+/).filter(Boolean).length
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function maskAllowedMechanics(text: string): string {
|
|
170
|
+
let masked = text
|
|
171
|
+
for (const literal of ALLOWED_MECHANICS) {
|
|
172
|
+
masked = masked.split(literal).join(' ')
|
|
173
|
+
}
|
|
174
|
+
return masked
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The four columns `enforcement.md` actually renders — the row's own name plus
|
|
179
|
+
* `Summary`, `Category`, `Description` — and the introduction above the first
|
|
180
|
+
* ring. Everything else in those tables is enforcing text that never reaches a
|
|
181
|
+
* page.
|
|
182
|
+
*/
|
|
183
|
+
export function enforcementPublishedText(content: string): string[] {
|
|
184
|
+
const lines = content.split('\n')
|
|
185
|
+
const out: string[] = []
|
|
186
|
+
|
|
187
|
+
const firstRing = lines.findIndex((line) => /^##\s+Ring 0\b/.test(line))
|
|
188
|
+
out.push((firstRing === -1 ? lines : lines.slice(0, firstRing)).join('\n'))
|
|
189
|
+
|
|
190
|
+
const publishedHeaders = ['summary', 'category', 'description']
|
|
191
|
+
for (const ring of [0, 1, 2]) {
|
|
192
|
+
const headingLine = findHeadingLine(lines, new RegExp(`^##\\s+Ring ${ring}\\b`))
|
|
193
|
+
if (headingLine === null) continue
|
|
194
|
+
const table = findTable(lines, headingLine + 1)
|
|
195
|
+
if (!table) continue
|
|
196
|
+
const indices = publishedHeaders
|
|
197
|
+
.map((name) => table.headers.findIndex((h) => h.trim().toLowerCase() === name))
|
|
198
|
+
.filter((i) => i !== -1)
|
|
199
|
+
for (const row of table.rows) {
|
|
200
|
+
out.push(row.cells[0] ?? '')
|
|
201
|
+
for (const i of indices) out.push(row.cells[i] ?? '')
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return out.filter((text) => text.trim().length > 0)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The readability rules, callable on any published text — not just a doc body.
|
|
210
|
+
*
|
|
211
|
+
* Exported because `/docs/actions` renders prose that lives in TypeScript
|
|
212
|
+
* (`ACTIONS` in `actions.ts`), not in a markdown file, so `evaluatePublishedProse`
|
|
213
|
+
* never sees it. A whole published page was unguarded on that technicality;
|
|
214
|
+
* `actions.test.ts` closes it by running this over every entry.
|
|
215
|
+
*/
|
|
216
|
+
export function readabilityErrors(where: string, texts: string[]): string[] {
|
|
217
|
+
const errors: string[] = []
|
|
218
|
+
checkReadability(where, texts, errors)
|
|
219
|
+
return errors
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function checkReadability(where: string, texts: string[], errors: string[]): void {
|
|
223
|
+
const seen = new Set<string>()
|
|
224
|
+
for (const raw of texts) {
|
|
225
|
+
const text = maskAllowedMechanics(raw)
|
|
226
|
+
for (const rule of PROSE_RULES) {
|
|
227
|
+
const match = text.match(rule.pattern)
|
|
228
|
+
if (!match) continue
|
|
229
|
+
const token = (match[0] ?? '').trim()
|
|
230
|
+
const key = `${rule.id}:${token}`
|
|
231
|
+
if (seen.has(key)) continue
|
|
232
|
+
seen.add(key)
|
|
233
|
+
errors.push(`C7: published text in "${where}" contains ${rule.what} a reader cannot resolve: "${token}"`)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function checkStructure(relPath: string, body: string, blocks: readonly string[], errors: string[]): void {
|
|
239
|
+
const short = extractShortVersion(body)
|
|
240
|
+
if (short === null) {
|
|
241
|
+
errors.push(
|
|
242
|
+
`C7: surfaced doc "${relPath}" has no "## ${SHORT_VERSION_HEADING}" section — the published page would have nothing binding to show a reader`
|
|
243
|
+
)
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let cursor = 0
|
|
248
|
+
for (const block of blocks) {
|
|
249
|
+
const at = short.indexOf(`**${block}`, cursor)
|
|
250
|
+
if (at === -1) {
|
|
251
|
+
errors.push(`C7: "${relPath}" short version is missing the "${block}" block (all four blocks always appear)`)
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
cursor = at + block.length
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const words = countWords(short)
|
|
258
|
+
if (words < SHORT_VERSION_MIN_WORDS) {
|
|
259
|
+
errors.push(
|
|
260
|
+
`C7: "${relPath}" short version is ${words} words, under the ${SHORT_VERSION_MIN_WORDS}-word floor — something was dropped`
|
|
261
|
+
)
|
|
262
|
+
} else if (words > SHORT_VERSION_MAX_WORDS) {
|
|
263
|
+
errors.push(
|
|
264
|
+
`C7: "${relPath}" short version is ${words} words, over the ${SHORT_VERSION_MAX_WORDS}-word ceiling — state the rule, don't summarise the reference`
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Runs both checks over the published set only. `entries` may hold every doc
|
|
271
|
+
* under `aeg-root/`; `surfacedPaths` (the model-backed allowlist) is what
|
|
272
|
+
* narrows it, so this check can never publish-check a doc the site does not
|
|
273
|
+
* publish, nor miss one it does.
|
|
274
|
+
*/
|
|
275
|
+
export function evaluatePublishedProse(
|
|
276
|
+
entries: PublishedProseEntry[],
|
|
277
|
+
surfacedPaths: ReadonlySet<string> = new Set()
|
|
278
|
+
): PublishedProseResult {
|
|
279
|
+
const errors: string[] = []
|
|
280
|
+
const notes: string[] = []
|
|
281
|
+
|
|
282
|
+
const surfaced = entries.filter((e) => isSurfacedDoc(e.relPath, e.frontmatter, surfacedPaths))
|
|
283
|
+
|
|
284
|
+
for (const entry of surfaced) {
|
|
285
|
+
// Frontmatter renders too: `title` is the page heading and the sidebar
|
|
286
|
+
// entry, `description` is the page metadata. Checking only the body left
|
|
287
|
+
// both unguarded — a citation in a title reaches a reader exactly as
|
|
288
|
+
// surely as one in a paragraph.
|
|
289
|
+
checkReadability(entry.relPath, [entry.frontmatter.title ?? '', entry.frontmatter.description ?? ''], errors)
|
|
290
|
+
|
|
291
|
+
const blocks = blocksFor(entry.relPath)
|
|
292
|
+
if (blocks) {
|
|
293
|
+
checkStructure(entry.relPath, entry.body, blocks, errors)
|
|
294
|
+
const short = extractShortVersion(entry.body)
|
|
295
|
+
if (short !== null) checkReadability(entry.relPath, [short], errors)
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
if (entry.relPath === 'enforcement.md') {
|
|
299
|
+
checkReadability(entry.relPath, enforcementPublishedText(entry.body), errors)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (surfaced.length === 0) notes.push('C7: no surfaced docs to check.')
|
|
304
|
+
|
|
305
|
+
return { errors, notes }
|
|
306
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical "surfaced doc" manifest for `aeg-root/`. Defines, as
|
|
3
|
+
* data, which docs a public AEG page shows. The rule is **model-backed**: a
|
|
4
|
+
* doc is surfaced if and only if a `DiagramModel` node points at it.
|
|
5
|
+
* The same `DiagramModel` that `/docs/harness` renders is the allowlist for
|
|
6
|
+
* `/docs/reference` — the two surfaces are two renderers of one model, and the doc that
|
|
7
|
+
* backs no reachable node stops being public, every build.
|
|
8
|
+
*
|
|
9
|
+
* The node→doc mapping (`modelBackedDocPaths`) is the same one `read-more.ts`
|
|
10
|
+
* uses to resolve a node's "Read more" target: gate/check nodes back
|
|
11
|
+
* `enforcement.md`; a role node backs `roles/<id>.md`; a contract node backs
|
|
12
|
+
* `contracts/<id>.md`. `action`/`ring` nodes back no `aeg-root/**` document
|
|
13
|
+
* (an action's source is `packages/aeg-core/src/actions.ts`, a ring is a
|
|
14
|
+
* summary label), so they add nothing to the set. Today that is 16 files —
|
|
15
|
+
* `enforcement.md` + the 9 `roles/*.md` + the 6 `contracts/*.md`.
|
|
16
|
+
*
|
|
17
|
+
* This is the single source of truth the C6 docs-coherence check
|
|
18
|
+
* (`docs-coherence.ts`) and Vinaya's `/docs` loader both consume. There is
|
|
19
|
+
* **no second, path-based exclusion rule** — a competing rule is the failure
|
|
20
|
+
* mode this manifest exists to prevent, and the model-backed set replaced the
|
|
21
|
+
* old path-exclusion rules outright rather than sitting beside them. The only
|
|
22
|
+
* escape hatch is the per-doc `surfaced` frontmatter override, which wins in
|
|
23
|
+
* both directions.
|
|
24
|
+
*
|
|
25
|
+
* Purity: this module imports only the `DiagramModel` **type** from the
|
|
26
|
+
* diagram layer — no runtime coupling, no I/O. The doctrine is read (and the
|
|
27
|
+
* model derived) by the caller, which passes the derived path set in; aeg-core
|
|
28
|
+
* stays zero-I/O (#372/#382/#506).
|
|
29
|
+
*
|
|
30
|
+
* Paths are relative to `aeg-root/` (e.g. `roles/developer.md`), matching
|
|
31
|
+
* `DocFrontmatter`'s existing convention.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type { DiagramModel } from '../diagram-model'
|
|
35
|
+
import type { DocFrontmatter } from './types'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The set of `aeg-root/`-relative doc paths a `DiagramModel` points at — the
|
|
39
|
+
* allowlist. Mirrors `read-more.ts`'s `docRoute` resolution exactly, so
|
|
40
|
+
* `/docs/reference`'s surfaced set and `/docs/harness`'s "Read more" targets can never
|
|
41
|
+
* name different files: gate/check → `enforcement.md`, role → `roles/<id>.md`,
|
|
42
|
+
* contract → `contracts/<id>.md`. A node's `label` is its doctrine id
|
|
43
|
+
* (`role.roleId` / `contract.contractId`), and every `roles/*.md` /
|
|
44
|
+
* `contracts/*.md` file is named `<id>.md` — one convention backs both the
|
|
45
|
+
* GitHub path and the docs route.
|
|
46
|
+
*/
|
|
47
|
+
export function modelBackedDocPaths(model: DiagramModel): Set<string> {
|
|
48
|
+
const paths = new Set<string>()
|
|
49
|
+
for (const node of model.nodes) {
|
|
50
|
+
if (node.kind === 'gate' || node.kind === 'check') paths.add('enforcement.md')
|
|
51
|
+
else if (node.kind === 'role') paths.add(`roles/${node.label}.md`)
|
|
52
|
+
else if (node.kind === 'contract') paths.add(`contracts/${node.label}.md`)
|
|
53
|
+
}
|
|
54
|
+
return paths
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const NO_MODEL_BACKED_PATHS: ReadonlySet<string> = new Set()
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether `relPath` (relative to `aeg-root/`) is a surfaced doc. A boolean
|
|
61
|
+
* `surfaced` frontmatter field always wins over the model — the escape hatch
|
|
62
|
+
* in both directions. Otherwise the doc is surfaced iff it is in
|
|
63
|
+
* `surfacedPaths` (the model-backed allowlist from `modelBackedDocPaths`).
|
|
64
|
+
*
|
|
65
|
+
* `surfacedPaths` is optional so a caller that has no model surfaces nothing
|
|
66
|
+
* by default rather than everything — the safe direction for a rule whose
|
|
67
|
+
* whole point is that a doc publishes only when a node points at it.
|
|
68
|
+
*/
|
|
69
|
+
export function isSurfacedDoc(
|
|
70
|
+
relPath: string,
|
|
71
|
+
frontmatter: Pick<DocFrontmatter, 'surfaced'>,
|
|
72
|
+
surfacedPaths: ReadonlySet<string> = NO_MODEL_BACKED_PATHS
|
|
73
|
+
): boolean {
|
|
74
|
+
if (typeof frontmatter.surfaced === 'boolean') return frontmatter.surfaced
|
|
75
|
+
return surfacedPaths.has(relPath)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type SurfacedManifestEntry = {
|
|
79
|
+
relPath: string
|
|
80
|
+
frontmatter: Pick<DocFrontmatter, 'surfaced'>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Filters a list of parsed doc entries down to the surfaced subset. */
|
|
84
|
+
export function surfacedDocs(
|
|
85
|
+
entries: SurfacedManifestEntry[],
|
|
86
|
+
surfacedPaths: ReadonlySet<string> = NO_MODEL_BACKED_PATHS
|
|
87
|
+
): string[] {
|
|
88
|
+
return entries.filter((e) => isSurfacedDoc(e.relPath, e.frontmatter, surfacedPaths)).map((e) => e.relPath)
|
|
89
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type DocFrontmatter = {
|
|
2
|
+
title?: string
|
|
3
|
+
sidebarTitle?: string
|
|
4
|
+
description?: string
|
|
5
|
+
section?: string
|
|
6
|
+
order?: number
|
|
7
|
+
parent?: string
|
|
8
|
+
surfaced?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type Doc = {
|
|
12
|
+
slug: string
|
|
13
|
+
title: string
|
|
14
|
+
sidebarTitle?: string
|
|
15
|
+
description?: string
|
|
16
|
+
section: string
|
|
17
|
+
order: number
|
|
18
|
+
href: string
|
|
19
|
+
filePath: string
|
|
20
|
+
children?: Doc[]
|
|
21
|
+
parentSlug?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type DocSection = {
|
|
25
|
+
id: string
|
|
26
|
+
label: string
|
|
27
|
+
docs: Doc[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type DocNav = {
|
|
31
|
+
sections: DocSection[]
|
|
32
|
+
flat: Doc[]
|
|
33
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
* Zero I/O: every input (file paths + contents) is read by the adapter and
|
|
21
|
+
* passed in.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export type PortabilitySourceFile = { path: string; content: string }
|
|
25
|
+
|
|
26
|
+
export type PortabilityFinding = {
|
|
27
|
+
file: string
|
|
28
|
+
line: number
|
|
29
|
+
cited: string
|
|
30
|
+
message: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** `aeg-root/**` by default — what this repo's own package ships and every adopter installs read-only. */
|
|
34
|
+
const DEFAULT_SHIPS_PREFIX = 'aeg-root/'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Doctrine-relative (`roles/`, `contracts/`, `skills/`) and adopter-owned
|
|
38
|
+
* (`.github/`, `.vinaya/`, `.claude/`) top segments — the two portable
|
|
39
|
+
* classes measured in the task-234 corpus scan. `aeg-root/` itself is
|
|
40
|
+
* deliberately NOT a static entry here: it is always the CALLER-supplied
|
|
41
|
+
* `shipsPrefix`, checked dynamically in `isPortable` below, so an adopter
|
|
42
|
+
* who configures a non-default `doctrineRoot` gets that root treated as
|
|
43
|
+
* portable too — a static `'aeg-root/'` entry would falsely flag every one
|
|
44
|
+
* of that adopter's own self-citations as non-portable. A citation whose
|
|
45
|
+
* top segment falls outside this list (and isn't the ships prefix) is
|
|
46
|
+
* never assumed portable, however plausible it looks; it is a finding, and
|
|
47
|
+
* someone extends this list deliberately once its side of the
|
|
48
|
+
* classification is decided.
|
|
49
|
+
*/
|
|
50
|
+
const STATIC_PORTABLE_PREFIXES: readonly string[] = [
|
|
51
|
+
'roles/',
|
|
52
|
+
'contracts/',
|
|
53
|
+
'skills/',
|
|
54
|
+
'.github/',
|
|
55
|
+
'.vinaya/',
|
|
56
|
+
'.claude/',
|
|
57
|
+
// Not in the task-234 corpus-scan table, but verified while reading the
|
|
58
|
+
// corpus: `.git/hooks/*` and `.husky/*` are cited only as the pre-commit/
|
|
59
|
+
// pre-push hook locations every git repo (and every husky-adopting repo)
|
|
60
|
+
// genuinely has — adopter-owned by construction, the same class as
|
|
61
|
+
// `.github/`/`.vinaya/`/`.claude/` above.
|
|
62
|
+
'.git/',
|
|
63
|
+
'.husky/'
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The three illustrative placeholders measured in the corpus — a
|
|
68
|
+
* `[path/inside/the/...]`-shaped fill-in-the-blank in a template, never a
|
|
69
|
+
* real citation. Exempted by literal, not by pattern, so a real path that
|
|
70
|
+
* happens to share a prefix with one of these is never accidentally waved
|
|
71
|
+
* through.
|
|
72
|
+
*/
|
|
73
|
+
const EXEMPT_LITERALS: ReadonlySet<string> = new Set([
|
|
74
|
+
'path/inside/the/shipped/diff.ts',
|
|
75
|
+
'path/inside/the/surface.ts',
|
|
76
|
+
'apps/x/specs/...'
|
|
77
|
+
])
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A cited path, inline-backtick-delimited, shaped like a repo-relative
|
|
81
|
+
* path: starts with a word/dot/dash character (never `/` — that shape is a
|
|
82
|
+
* web route, e.g. `` `/docs/state-machine` ``, not a repo path), contains at
|
|
83
|
+
* least one `/`, and carries no character a URL or a prose fragment would
|
|
84
|
+
* (no `:`, no whitespace, no parens) — which also excludes a link like
|
|
85
|
+
* `` `https://vinaya.dev` `` without a separate URL-shaped exclusion.
|
|
86
|
+
*/
|
|
87
|
+
const CITED_PATH_PATTERN = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]*)+$/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Top segments that are never a repo path, verified against every one of
|
|
91
|
+
* their occurrences in the real corpus rather than assumed: `origin/`/`refs/`
|
|
92
|
+
* are git-ref namespace (`origin/main`, `refs/pull/N/merge`), and `vinaya/`/
|
|
93
|
+
* `fix/` are this doctrine's own GitHub-label and branch-name-example
|
|
94
|
+
* conventions (`vinaya/blocked`, `fix/brief-gate-nontask`) — a citation kind
|
|
95
|
+
* this check does not judge at all, portable or not, the same way it never
|
|
96
|
+
* tries to also judge a forge number or a tranche slug.
|
|
97
|
+
*
|
|
98
|
+
* **A closed, tested set, not a growable escape hatch.** Excluding a top
|
|
99
|
+
* segment here means every citation under it is invisible to this check —
|
|
100
|
+
* never flagged, however non-portable it would otherwise be — so this set
|
|
101
|
+
* carries exactly the corpus-verified exceptions above and nothing else.
|
|
102
|
+
* `doctrine-portability.test.ts` locks its exact membership; an addition
|
|
103
|
+
* that doesn't also update that lock is a bug, not a silent expansion.
|
|
104
|
+
* Exported for that lock, not for callers to extend at runtime.
|
|
105
|
+
*/
|
|
106
|
+
export const NON_PATH_TOP_SEGMENTS: ReadonlySet<string> = new Set(['origin', 'refs', 'HEAD', 'vinaya', 'fix'])
|
|
107
|
+
|
|
108
|
+
/** Every inline-backtick span in `content`, tested against `CITED_PATH_PATTERN`. */
|
|
109
|
+
function extractCitedPaths(content: string): { cited: string; index: number }[] {
|
|
110
|
+
const found: { cited: string; index: number }[] = []
|
|
111
|
+
const spanPattern = /`([^`\n]+)`/g
|
|
112
|
+
let match: RegExpExecArray | null = spanPattern.exec(content)
|
|
113
|
+
while (match !== null) {
|
|
114
|
+
const cited = match[1] ?? ''
|
|
115
|
+
const topSegment = cited.slice(0, cited.indexOf('/'))
|
|
116
|
+
if (CITED_PATH_PATTERN.test(cited) && !NON_PATH_TOP_SEGMENTS.has(topSegment)) {
|
|
117
|
+
found.push({ cited, index: match.index })
|
|
118
|
+
}
|
|
119
|
+
match = spanPattern.exec(content)
|
|
120
|
+
}
|
|
121
|
+
return found
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function lineAtIndex(content: string, index: number): number {
|
|
125
|
+
let line = 1
|
|
126
|
+
for (let i = 0; i < index; i++) {
|
|
127
|
+
if (content.charCodeAt(i) === 10) line++
|
|
128
|
+
}
|
|
129
|
+
return line
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* True iff `cited`'s top segment is portable: the caller-supplied
|
|
134
|
+
* `shipsPrefix` itself (doctrine citing its own tree, wherever that tree
|
|
135
|
+
* actually lives for this caller), or one of the static portable prefixes.
|
|
136
|
+
*/
|
|
137
|
+
function isPortable(cited: string, shipsPrefix: string): boolean {
|
|
138
|
+
if (cited.startsWith(shipsPrefix)) return true
|
|
139
|
+
return STATIC_PORTABLE_PREFIXES.some((prefix) => cited.startsWith(prefix))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Sweeps every file under `shipsPrefix` for a cited path whose top segment
|
|
144
|
+
* is not allow-listed portable. Files outside `shipsPrefix` are out of
|
|
145
|
+
* scope entirely — this check only judges what the shipped doctrine tree
|
|
146
|
+
* itself cites, never a repo's other source.
|
|
147
|
+
*/
|
|
148
|
+
export function checkDoctrinePortability(
|
|
149
|
+
files: readonly PortabilitySourceFile[],
|
|
150
|
+
shipsPrefix: string = DEFAULT_SHIPS_PREFIX
|
|
151
|
+
): PortabilityFinding[] {
|
|
152
|
+
const findings: PortabilityFinding[] = []
|
|
153
|
+
|
|
154
|
+
for (const file of files) {
|
|
155
|
+
if (!file.path.startsWith(shipsPrefix)) continue
|
|
156
|
+
|
|
157
|
+
for (const { cited, index } of extractCitedPaths(file.content)) {
|
|
158
|
+
if (EXEMPT_LITERALS.has(cited)) continue
|
|
159
|
+
if (isPortable(cited, shipsPrefix)) continue
|
|
160
|
+
findings.push({
|
|
161
|
+
file: file.path,
|
|
162
|
+
line: lineAtIndex(file.content, index),
|
|
163
|
+
cited,
|
|
164
|
+
message: `cites "${cited}", a path that only exists in the authoring repository — not portable doctrine`
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return findings
|
|
170
|
+
}
|
|
@@ -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
|
+
}
|