@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,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single node → `/docs` route derivation. Nav construction
|
|
3
|
+
* (`load-aeg-docs.ts`), the "Read more" resolver (`read-more.ts`), and the
|
|
4
|
+
* harness map (`/docs` `page.tsx`) all resolve a `DiagramNode` to its docs
|
|
5
|
+
* location through THIS function — one source of truth, so a card's deep-link,
|
|
6
|
+
* a "Read more" anchor, and a ring/action page's heading `id` can never point
|
|
7
|
+
* at three different places for the same node. A parallel, hand-maintained
|
|
8
|
+
* slug list is the exact failure mode this replaces (the same discipline
|
|
9
|
+
* `surfaced-manifest.ts` holds for the file allowlist).
|
|
10
|
+
*
|
|
11
|
+
* Granularity follows content size. A role or contract is a whole
|
|
12
|
+
* `aeg-root/**.md` file, so it keeps its own page and needs no anchor. A gate
|
|
13
|
+
* or check is one row of `enforcement.md`, and an action is one entry of
|
|
14
|
+
* `ACTIONS`, so each is an `#`-anchored section inside a grouping page
|
|
15
|
+
* (`/docs/rings/ring-<n>` or `/docs/actions`).
|
|
16
|
+
*
|
|
17
|
+
* The anchor slug is recomputed here from the node's DISPLAY form, not read
|
|
18
|
+
* verbatim off the node id. `node.id`/`node.label` keep the full doctrine
|
|
19
|
+
* text — G-codes included — because the registry check (G1-G5,
|
|
20
|
+
* `registry-checks.ts`) matches on that exact id; a slug that a reader
|
|
21
|
+
* follows into a URL fragment has no such constraint, so it is cleaned at
|
|
22
|
+
* this one edge instead. Both the heading (`humanLabel`/`shortLabel`, in
|
|
23
|
+
* `apps/vinaya/web`) and this anchor apply the same two rules — strip a
|
|
24
|
+
* leading G-code, cut a pathologically long name at its first clause — so a
|
|
25
|
+
* heading and its anchor still agree, just no longer byte-identical to the
|
|
26
|
+
* doctrine id. A node whose display form needs no cleanup still gets the
|
|
27
|
+
* same slug it always did, since stripping/cutting are no-ops on it.
|
|
28
|
+
*
|
|
29
|
+
* Zero I/O, additive export — takes an already-derived node, returns data,
|
|
30
|
+
* never reads a file (aeg-core purity, #372/#382/#506).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import type { DiagramNode } from '../diagram-model'
|
|
34
|
+
|
|
35
|
+
export type NodeDocRoute = {
|
|
36
|
+
/** The page a node lives on. */
|
|
37
|
+
route: string
|
|
38
|
+
/** The heading anchor within that page, or `null` for a file-sized node
|
|
39
|
+
* (role/contract) that owns its whole page. */
|
|
40
|
+
slug: string | null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A pathologically long anchor is cut at its first clause boundary rather
|
|
44
|
+
* than published whole. 100 is chosen against the current doctrine, not a
|
|
45
|
+
* round guess: the longest anchor slug outside the one row this exists to
|
|
46
|
+
* fix is 84 characters (a ring-0 gate's compound name), so 100 leaves that
|
|
47
|
+
* row untouched while still catching genuinely oversized names — the one
|
|
48
|
+
* 127-character row this was written for included. */
|
|
49
|
+
const MAX_ANCHOR_SLUG_LENGTH = 100
|
|
50
|
+
|
|
51
|
+
/** Strips a leading `g<n>-` code off an already-slugified anchor. Operates on
|
|
52
|
+
* the slug form (not `node.label`) so a node whose id isn't `slugify(label)`
|
|
53
|
+
* to begin with — an `action`, whose id is the hand-authored `ACTIONS` entry
|
|
54
|
+
* id, not a re-derivation of its label — is never touched: no G-coded or
|
|
55
|
+
* over-length action exists today, so this function is a no-op for every one
|
|
56
|
+
* of them, exactly preserving their existing anchors. */
|
|
57
|
+
function stripGCodeSlug(slug: string): string {
|
|
58
|
+
return slug.replace(/^g\d+-/, '')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Strips the same `G<n> — ` doctrine code off raw label text, for the
|
|
62
|
+
* over-length fallback below. Same contract as `humanLabel` in
|
|
63
|
+
* `apps/vinaya/web`'s `display-label.ts` (duplicated rather than shared:
|
|
64
|
+
* aeg-core cannot import from the web app, and this rule is small enough
|
|
65
|
+
* that keeping two copies in sync by inspection is cheaper than a new
|
|
66
|
+
* cross-package export). */
|
|
67
|
+
function stripGCodeLabel(label: string): string {
|
|
68
|
+
return label.replace(/^G\d+\s*—\s*/, '')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Cuts at the first clause boundary, same separator set `shortLabel` uses
|
|
72
|
+
* in `display-label.ts` — kept in sync with that function's comment by
|
|
73
|
+
* inspection, for the same reason `stripGCodeLabel` is duplicated above. */
|
|
74
|
+
function firstClause(label: string): string {
|
|
75
|
+
return label.split(/ \(| \/ |—|–|: /)[0]?.trim() ?? label
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Same cleanup `diagram-model.ts`'s own (private, un-exported) `slugify`
|
|
79
|
+
* applies when it stamps a node id — duplicated here rather than imported so
|
|
80
|
+
* this file never needs `diagram-model.ts` to export it. Reached only by the
|
|
81
|
+
* over-length fallback below, never by the common case. */
|
|
82
|
+
function slugifyText(text: string): string {
|
|
83
|
+
return text
|
|
84
|
+
.toLowerCase()
|
|
85
|
+
.replace(/`/g, '')
|
|
86
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
87
|
+
.replace(/^-+|-+$/g, '')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The anchor slug for a node's display form. Strips a G-code off the
|
|
91
|
+
* existing id-stamped slug first — a byte-preserving operation for every
|
|
92
|
+
* node that never had one. Only if the result is STILL too long to publish
|
|
93
|
+
* does this fall back to re-deriving a short slug from `node.label`'s first
|
|
94
|
+
* clause; that fallback is the one path that can produce a slug other than
|
|
95
|
+
* a prefix of the id-stamped one, and today only one row (a 127-character
|
|
96
|
+
* ring-0 gate name) reaches it. */
|
|
97
|
+
function nodeSlug(node: DiagramNode): string {
|
|
98
|
+
const rawSlug = node.id.slice(node.kind.length + 1)
|
|
99
|
+
const stripped = stripGCodeSlug(rawSlug)
|
|
100
|
+
if (stripped.length <= MAX_ANCHOR_SLUG_LENGTH) return stripped
|
|
101
|
+
return slugifyText(firstClause(stripGCodeLabel(node.label)))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a node to its `/docs` route + anchor slug. Returns `null` for a kind
|
|
106
|
+
* with no docs surface (there is none today — every kind resolves).
|
|
107
|
+
*/
|
|
108
|
+
export function nodeDocRoute(node: DiagramNode): NodeDocRoute | null {
|
|
109
|
+
switch (node.kind) {
|
|
110
|
+
case 'role':
|
|
111
|
+
return { route: `/docs/roles/${node.label}`, slug: null }
|
|
112
|
+
case 'contract':
|
|
113
|
+
return { route: `/docs/contracts/${node.label}`, slug: null }
|
|
114
|
+
case 'gate':
|
|
115
|
+
case 'check':
|
|
116
|
+
if (node.ringIndex === undefined) return null
|
|
117
|
+
return { route: `/docs/rings/ring-${node.ringIndex}`, slug: nodeSlug(node) }
|
|
118
|
+
case 'action':
|
|
119
|
+
return { route: '/docs/actions', slug: nodeSlug(node) }
|
|
120
|
+
case 'ring':
|
|
121
|
+
if (node.ringIndex === undefined) return null
|
|
122
|
+
return { route: `/docs/rings/ring-${node.ringIndex}`, slug: null }
|
|
123
|
+
default:
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The full in-app href — `route` plus a `#slug` fragment when the node is an
|
|
129
|
+
* anchored section. The form the map and `read-more.ts` link to directly. */
|
|
130
|
+
export function nodeDocHref(node: DiagramNode): string | null {
|
|
131
|
+
const resolved = nodeDocRoute(node)
|
|
132
|
+
if (!resolved) return null
|
|
133
|
+
return resolved.slug ? `${resolved.route}#${resolved.slug}` : resolved.route
|
|
134
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import matter from 'gray-matter'
|
|
2
|
+
import type { DocFrontmatter } from './types'
|
|
3
|
+
|
|
4
|
+
export type ParsedDoc = {
|
|
5
|
+
frontmatter: DocFrontmatter
|
|
6
|
+
body: string
|
|
7
|
+
firstH1?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const H1_PATTERN = /^\s*#\s+(.+?)\s*$/m
|
|
11
|
+
|
|
12
|
+
export function parseDocFrontmatter(raw: string): ParsedDoc {
|
|
13
|
+
const { data, content } = matter(raw)
|
|
14
|
+
const fm = data as Record<string, unknown>
|
|
15
|
+
|
|
16
|
+
const frontmatter: DocFrontmatter = {}
|
|
17
|
+
if (typeof fm.title === 'string') frontmatter.title = fm.title
|
|
18
|
+
if (typeof fm.sidebarTitle === 'string') {
|
|
19
|
+
frontmatter.sidebarTitle = fm.sidebarTitle
|
|
20
|
+
} else if (typeof fm.sidebar_title === 'string') {
|
|
21
|
+
frontmatter.sidebarTitle = fm.sidebar_title
|
|
22
|
+
}
|
|
23
|
+
if (typeof fm.description === 'string') frontmatter.description = fm.description
|
|
24
|
+
if (typeof fm.section === 'string') frontmatter.section = fm.section
|
|
25
|
+
if (typeof fm.order === 'number') frontmatter.order = fm.order
|
|
26
|
+
if (typeof fm.parent === 'string') frontmatter.parent = fm.parent
|
|
27
|
+
if (typeof fm.surfaced === 'boolean') frontmatter.surfaced = fm.surfaced
|
|
28
|
+
|
|
29
|
+
const h1Match = content.match(H1_PATTERN)
|
|
30
|
+
const firstH1 = h1Match?.[1]?.trim()
|
|
31
|
+
|
|
32
|
+
return { frontmatter, body: content, firstH1 }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function deriveTitle(parsed: ParsedDoc, fallbackFromPath: string): string {
|
|
36
|
+
if (parsed.frontmatter.title) return parsed.frontmatter.title
|
|
37
|
+
if (parsed.firstH1) return parsed.firstH1
|
|
38
|
+
return fallbackFromPath
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function stripLeadingH1(body: string): string {
|
|
42
|
+
return body.replace(/^\s*#\s+.*\n+/, '')
|
|
43
|
+
}
|
|
@@ -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
|
+
}
|