@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,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `.vinaya/doc-owners` manifest parser and the C5 code→doc
|
|
3
|
+
* coverage evaluator (state-machine.md Section 15). Pure — I/O (reading the
|
|
4
|
+
* manifest off disk, checking pointer existence) is injected by the caller.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { isCodeFile } from './file-classify'
|
|
8
|
+
import { WAIVER_LABEL } from './waiver-label'
|
|
9
|
+
|
|
10
|
+
export const DOC_OWNERS_PATH = '.vinaya/doc-owners'
|
|
11
|
+
|
|
12
|
+
export type DocOwnersBinding = { glob: string; pointer: string; lineNum: number }
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What a caller found where the manifest should be.
|
|
16
|
+
*
|
|
17
|
+
* `absent` — a repo that never configured doc ownership. Legitimately dormant.
|
|
18
|
+
* `empty` — a manifest that exists but has nothing in it. This is the shape a
|
|
19
|
+
* misresolved repo root produces, and reporting it as success is how a broken
|
|
20
|
+
* derivation passes for a real one. It is a refusal, not a dormancy.
|
|
21
|
+
* `present` — parse it.
|
|
22
|
+
*
|
|
23
|
+
* Split out of `verify-dispatch --surfaces` so the three-way decision is
|
|
24
|
+
* testable: the CLI chdirs to the repo root before reading, so the branch is
|
|
25
|
+
* unreachable from a test as long as it lives inside the command.
|
|
26
|
+
*/
|
|
27
|
+
export type DocOwnersManifestState = 'absent' | 'empty' | 'present'
|
|
28
|
+
|
|
29
|
+
export function classifyDocOwnersManifest(content: string | null): DocOwnersManifestState {
|
|
30
|
+
if (content === null) return 'absent'
|
|
31
|
+
return content.trim() === '' ? 'empty' : 'present'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type C5Result = { errors: string[]; notes: string[] }
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse the doc-owners file. Each non-blank, non-comment line is
|
|
38
|
+
* `<code-glob> <doc-pointer>`
|
|
39
|
+
*
|
|
40
|
+
* Comments start with `#`. Pointer is in-repo path, path#anchor, or URL.
|
|
41
|
+
* Returns the parsed bindings + any malformed-line errors.
|
|
42
|
+
*/
|
|
43
|
+
export function parseDocOwners(content: string): { bindings: DocOwnersBinding[]; errors: string[] } {
|
|
44
|
+
const bindings: DocOwnersBinding[] = []
|
|
45
|
+
const errors: string[] = []
|
|
46
|
+
const lines = content.split('\n')
|
|
47
|
+
for (let i = 0; i < lines.length; i++) {
|
|
48
|
+
const raw = lines[i] ?? ''
|
|
49
|
+
const stripped = raw.replace(/#.*$/, '').trim()
|
|
50
|
+
if (!stripped) continue
|
|
51
|
+
const parts = stripped.split(/\s+/)
|
|
52
|
+
if (parts.length < 2) {
|
|
53
|
+
errors.push(
|
|
54
|
+
`C5 doc-owners-parse: ${DOC_OWNERS_PATH}:${i + 1} — malformed binding (expected "<glob> <pointer>", got "${raw.trim()}").`
|
|
55
|
+
)
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
const [glob, ...pointerParts] = parts
|
|
59
|
+
bindings.push({ glob: glob ?? '', pointer: pointerParts.join(' '), lineNum: i + 1 })
|
|
60
|
+
}
|
|
61
|
+
return { bindings, errors }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Translate a doc-owners glob to a RegExp. Deliberately simple — only `*` and
|
|
66
|
+
* `**` are special; every other character is literal so Next.js dynamic-route
|
|
67
|
+
* segments like `[username]` match without escaping.
|
|
68
|
+
*/
|
|
69
|
+
export function globToRegex(pat: string): RegExp {
|
|
70
|
+
let re = '^'
|
|
71
|
+
let i = 0
|
|
72
|
+
while (i < pat.length) {
|
|
73
|
+
const c = pat[i]
|
|
74
|
+
if (c === '*') {
|
|
75
|
+
if (pat[i + 1] === '*') {
|
|
76
|
+
re += '.*'
|
|
77
|
+
i += 2
|
|
78
|
+
} else {
|
|
79
|
+
re += '[^/]*'
|
|
80
|
+
i += 1
|
|
81
|
+
}
|
|
82
|
+
} else if (/[.+^$|(){}[\]\\?]/.test(c as string)) {
|
|
83
|
+
re += `\\${c}`
|
|
84
|
+
i += 1
|
|
85
|
+
} else {
|
|
86
|
+
re += c
|
|
87
|
+
i += 1
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
re += '$'
|
|
91
|
+
return new RegExp(re)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function isUrlPointer(p: string): boolean {
|
|
95
|
+
return /^https?:\/\//.test(p)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function pointerToPath(p: string): string {
|
|
99
|
+
const idx = p.indexOf('#')
|
|
100
|
+
return idx === -1 ? p : p.slice(0, idx)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type DocAck = { surface: string; note: string }
|
|
104
|
+
export type DocNeutral = { surface: string; note: string }
|
|
105
|
+
|
|
106
|
+
// Separator between <pointer> and <note>. Tolerates em-dash (—),
|
|
107
|
+
// en-dash (–), or a plain ASCII hyphen-minus (-) with REQUIRED surrounding
|
|
108
|
+
// whitespace. Required whitespace around `-` disambiguates the separator
|
|
109
|
+
// from hyphens that legitimately appear inside pointers (e.g. `aeg-root`,
|
|
110
|
+
// `.claude/skills/ui-components/SKILL.md`). Non-greedy `(.+?)` for the
|
|
111
|
+
// pointer plus this anchored separator means the pointer naturally stops
|
|
112
|
+
// at the first valid separator without literally excluding `-` from
|
|
113
|
+
// the pointer character set.
|
|
114
|
+
const SEPARATOR = /(?:[ \t]*[—–][ \t]*|[ \t]+-[ \t]+)/.source
|
|
115
|
+
|
|
116
|
+
/** Shared by `Doc-ack:` and `Doc-neutral:` — both are `<field>: <pointer> <sep> <note>` PR-body lines. */
|
|
117
|
+
function readPointerNoteField(body: string, field: string): { surface: string; note: string }[] {
|
|
118
|
+
const out: { surface: string; note: string }[] = []
|
|
119
|
+
const re = new RegExp(`^[ \\t]*${field}[ \\t]*:[ \\t]*(.+?)${SEPARATOR}(.+?)[ \\t]*$`, 'gim')
|
|
120
|
+
for (const m of body.matchAll(re)) {
|
|
121
|
+
out.push({ surface: (m[1] ?? '').trim(), note: (m[2] ?? '').trim() })
|
|
122
|
+
}
|
|
123
|
+
return out
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function readDocAcks(body: string): DocAck[] {
|
|
127
|
+
return readPointerNoteField(body, 'Doc-ack')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* `Doc-neutral: <pointer> — <note>` — declares that the doc a fired binding
|
|
132
|
+
* would otherwise require is not owed because the matched code change is
|
|
133
|
+
* mechanically neutral (comment/whitespace-only). Mirrors `Doc-ack:`'s
|
|
134
|
+
* grammar deliberately, so it reads as the same PR-body-field family.
|
|
135
|
+
*
|
|
136
|
+
* The declaration alone is never sufficient — see `isMechanicallyNeutralDiff`
|
|
137
|
+
* below, which `evaluateC5` requires as corroborating evidence before this
|
|
138
|
+
* declaration satisfies a fired binding. An unevidenced declaration is a
|
|
139
|
+
* distinct failure (`C5 doc-neutral-unverified`), not a silent pass — that
|
|
140
|
+
* asymmetry is what keeps this from being a self-serve exemption.
|
|
141
|
+
*/
|
|
142
|
+
export function readDocNeutrals(body: string): DocNeutral[] {
|
|
143
|
+
return readPointerNoteField(body, 'Doc-neutral')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Full-line comment markers, scoped per source-language file extension. A
|
|
148
|
+
* marker is listed for a language ONLY if no legitimate code construct in
|
|
149
|
+
* that language can start a trimmed line with it — that constraint is what
|
|
150
|
+
* keeps this fail-closed rather than a bare per-line prefix table (review
|
|
151
|
+
* finding, task 4 code review): a flat asterisk/hash/double-dash/block-
|
|
152
|
+
* comment-delimiter table misclassifies real code as comment — a TS/JS
|
|
153
|
+
* generator method (`*gen() {`), a TS/JS private class field
|
|
154
|
+
* (`#count = 0`), SQL's `--` also being C-style pre-decrement (`--i;`), and
|
|
155
|
+
* a block-comment-close delimiter hiding trailing code on the same line are
|
|
156
|
+
* all real collisions, not hypothetical ones. `//` is the only marker kept
|
|
157
|
+
* for TS/JS/JSX because no such language construct starts a trimmed line
|
|
158
|
+
* with `//`. An unrecognized extension gets no safe marker at all, so
|
|
159
|
+
* `isMechanicallyNeutralDiff` can never classify it neutral — fail-closed
|
|
160
|
+
* under uncertainty, not an attempt at full per-language comment/string-
|
|
161
|
+
* region parsing (out of this task's bounded "comment/whitespace-only"
|
|
162
|
+
* scope).
|
|
163
|
+
*/
|
|
164
|
+
const LANGUAGE_COMMENT_PREFIXES: ReadonlyArray<{ test: RegExp; prefixes: readonly string[] }> = [
|
|
165
|
+
{ test: /\.(ts|tsx|js|jsx|mjs|cjs)$/, prefixes: ['//'] },
|
|
166
|
+
{ test: /(^|\/)(pre-push|pre-commit|pre-merge-commit)$/, prefixes: ['#'] },
|
|
167
|
+
{ test: /\.(sh|bash)$/, prefixes: ['#'] },
|
|
168
|
+
{ test: /\.ya?ml$/, prefixes: ['#'] },
|
|
169
|
+
{ test: /\.py$/, prefixes: ['#'] }
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
function commentPrefixesForPath(path: string): readonly string[] {
|
|
173
|
+
for (const { test, prefixes } of LANGUAGE_COMMENT_PREFIXES) {
|
|
174
|
+
if (test.test(path)) return prefixes
|
|
175
|
+
}
|
|
176
|
+
return []
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isNeutralDiffContentLine(line: string, prefixes: readonly string[]): boolean {
|
|
180
|
+
const t = line.trim()
|
|
181
|
+
if (t === '') return true
|
|
182
|
+
// A shebang is comment-shaped but changes what interpreter runs the
|
|
183
|
+
// file — real behavior, never neutral, even though it starts with `#`.
|
|
184
|
+
if (t.startsWith('#!')) return false
|
|
185
|
+
return prefixes.some((p) => t.startsWith(p))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The evidence half of the neutral-edit path: given a unified diff for one
|
|
190
|
+
* file and that file's path (for language-scoped comment-marker selection),
|
|
191
|
+
* true only if every added/removed line is comment-only or whitespace-only.
|
|
192
|
+
* Context lines and the `+++`/`---`/`@@` headers are not evidence either
|
|
193
|
+
* way. A diff with zero +/- content lines is not "a change" at all, so it
|
|
194
|
+
* returns false rather than vacuously true — there must be at least one
|
|
195
|
+
* actual edited line for a neutrality claim to mean anything. An
|
|
196
|
+
* unrecognized file extension (`commentPrefixesForPath` returns `[]`)
|
|
197
|
+
* always returns false — no fired binding on an unrecognized language can
|
|
198
|
+
* take the Doc-neutral path.
|
|
199
|
+
*/
|
|
200
|
+
export function isMechanicallyNeutralDiff(diffText: string, path: string): boolean {
|
|
201
|
+
const prefixes = commentPrefixesForPath(path)
|
|
202
|
+
let sawChange = false
|
|
203
|
+
for (const raw of diffText.split('\n')) {
|
|
204
|
+
if (raw.startsWith('+++ ') || raw.startsWith('--- ') || raw.startsWith('@@')) continue
|
|
205
|
+
if (raw.startsWith('+') || raw.startsWith('-')) {
|
|
206
|
+
if (!isNeutralDiffContentLine(raw.slice(1), prefixes)) return false
|
|
207
|
+
sawChange = true
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return sawChange
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Pure evaluator for the C5 doc-coverage check. The runtime wrapper reads
|
|
215
|
+
* `.vinaya/doc-owners` from disk and `PR_BODY` from env; this
|
|
216
|
+
* function takes both as inputs and an injectable file-exists for unit tests.
|
|
217
|
+
*
|
|
218
|
+
* Dormancy: a null `docOwnersContent` (absent file) OR no glob matching any
|
|
219
|
+
* changed code file produces an empty result — no errors, no notes.
|
|
220
|
+
*
|
|
221
|
+
* `waiverActive` is a single PR-wide boolean, not a per-binding
|
|
222
|
+
* pointer/reason lookup — it's the caller-resolved result of
|
|
223
|
+
* `isWaiverLabelActorVerified`, itself a mechanized read of a forge fact.
|
|
224
|
+
* There is no agent-emittable `Doc-waiver:` string anymore; a waiver is
|
|
225
|
+
* either verified true PR-wide, or not.
|
|
226
|
+
*
|
|
227
|
+
* `getDiff` is optional and injectable (unit tests; also absent whenever a
|
|
228
|
+
* caller has no diff-content source) — it resolves the unified diff for one
|
|
229
|
+
* matched code file, feeding the `Doc-neutral:` evidence check below. Its
|
|
230
|
+
* absence never produces a pass: a declared-but-unevidenced neutral claim
|
|
231
|
+
* fails the same as if it evidence didn't check out, preserving silence =
|
|
232
|
+
* failure (state-machine.md Section 15).
|
|
233
|
+
*/
|
|
234
|
+
export function evaluateC5(
|
|
235
|
+
changed: string[],
|
|
236
|
+
docOwnersContent: string | null,
|
|
237
|
+
prBody: string,
|
|
238
|
+
fileExists: (p: string) => boolean,
|
|
239
|
+
waiverActive: boolean,
|
|
240
|
+
getDiff?: (p: string) => string | null
|
|
241
|
+
): C5Result {
|
|
242
|
+
const out: C5Result = { errors: [], notes: [] }
|
|
243
|
+
|
|
244
|
+
if (docOwnersContent === null) return out
|
|
245
|
+
|
|
246
|
+
const { bindings, errors: parseErrors } = parseDocOwners(docOwnersContent)
|
|
247
|
+
for (const e of parseErrors) out.errors.push(e)
|
|
248
|
+
if (bindings.length === 0) return out
|
|
249
|
+
|
|
250
|
+
const codeFiles = changed.filter(isCodeFile)
|
|
251
|
+
const fired: { binding: DocOwnersBinding; matchedFiles: string[] }[] = []
|
|
252
|
+
for (const b of bindings) {
|
|
253
|
+
const re = globToRegex(b.glob)
|
|
254
|
+
const matchedFiles = codeFiles.filter((f) => re.test(f))
|
|
255
|
+
if (matchedFiles.length > 0) fired.push({ binding: b, matchedFiles })
|
|
256
|
+
}
|
|
257
|
+
if (fired.length === 0) return out
|
|
258
|
+
|
|
259
|
+
const acks = readDocAcks(prBody)
|
|
260
|
+
const neutrals = readDocNeutrals(prBody)
|
|
261
|
+
|
|
262
|
+
for (const { binding: b, matchedFiles } of fired) {
|
|
263
|
+
if (waiverActive) {
|
|
264
|
+
out.notes.push(`C5 doc-waiver active for ${b.pointer} (binding ${DOC_OWNERS_PATH}:${b.lineNum})`)
|
|
265
|
+
continue
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (isUrlPointer(b.pointer)) {
|
|
269
|
+
const acked = acks.some((a) => a.surface === b.pointer)
|
|
270
|
+
if (!acked) {
|
|
271
|
+
out.errors.push(
|
|
272
|
+
`C5 doc-coverage: code change matched ${DOC_OWNERS_PATH}:${b.lineNum} (glob \`${b.glob}\` → ${b.pointer}). External pointer requires \`Doc-ack: ${b.pointer} — <note>\` in the PR body, or an actor-verified \`${WAIVER_LABEL}\` label to skip.`
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
continue
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const pointerPath = pointerToPath(b.pointer)
|
|
279
|
+
if (!fileExists(pointerPath)) {
|
|
280
|
+
out.errors.push(
|
|
281
|
+
`C5 doc-owners-dangling: ${DOC_OWNERS_PATH}:${b.lineNum} points to ${b.pointer}, which does not exist on disk. Fix the binding or add the doc.`
|
|
282
|
+
)
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (changed.includes(pointerPath)) continue
|
|
287
|
+
|
|
288
|
+
const declared = neutrals.find((n) => n.surface === b.pointer)
|
|
289
|
+
if (declared) {
|
|
290
|
+
const evidenced =
|
|
291
|
+
getDiff !== undefined &&
|
|
292
|
+
matchedFiles.every((f) => {
|
|
293
|
+
const diff = getDiff(f)
|
|
294
|
+
return diff !== null && isMechanicallyNeutralDiff(diff, f)
|
|
295
|
+
})
|
|
296
|
+
if (evidenced) {
|
|
297
|
+
out.notes.push(
|
|
298
|
+
`C5 doc-neutral: ${b.pointer} not required for ${DOC_OWNERS_PATH}:${b.lineNum} (glob \`${b.glob}\`) — declared neutral ("${declared.note}"), diff of ${matchedFiles.join(', ')} confirmed comment/whitespace-only.`
|
|
299
|
+
)
|
|
300
|
+
continue
|
|
301
|
+
}
|
|
302
|
+
out.errors.push(
|
|
303
|
+
`C5 doc-neutral-unverified: ${DOC_OWNERS_PATH}:${b.lineNum} (glob \`${b.glob}\` → ${b.pointer}) declared \`Doc-neutral: ${b.pointer} — ${declared.note}\`, but the diff of ${matchedFiles.join(', ')} contains changes beyond comments/whitespace. Update ${pointerPath}, or have a principal apply the \`${WAIVER_LABEL}\` label.`
|
|
304
|
+
)
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
out.errors.push(
|
|
309
|
+
`C5 doc-coverage: code change matched ${DOC_OWNERS_PATH}:${b.lineNum} (glob \`${b.glob}\` → ${b.pointer}), but ${pointerPath} is not in the PR diff. Update it, declare \`Doc-neutral: ${b.pointer} — <why>\` if the change is genuinely mechanically-neutral, or have a principal apply the \`${WAIVER_LABEL}\` label.`
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return out
|
|
314
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Doc, DocNav, DocSection } from './types'
|
|
2
|
+
|
|
3
|
+
export type BuildDocNavOptions = {
|
|
4
|
+
sectionOrder?: string[]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function sectionIdOf(label: string): string {
|
|
8
|
+
return label
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
11
|
+
.replace(/^-+|-+$/g, '')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildDocNav(docs: Doc[], opts: BuildDocNavOptions = {}): DocNav {
|
|
15
|
+
const bySection = new Map<string, Doc[]>()
|
|
16
|
+
for (const doc of docs) {
|
|
17
|
+
const list = bySection.get(doc.section) ?? []
|
|
18
|
+
list.push(doc)
|
|
19
|
+
bySection.set(doc.section, list)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ordered: string[] = []
|
|
23
|
+
if (opts.sectionOrder) {
|
|
24
|
+
for (const label of opts.sectionOrder) if (bySection.has(label)) ordered.push(label)
|
|
25
|
+
}
|
|
26
|
+
for (const label of bySection.keys()) if (!ordered.includes(label)) ordered.push(label)
|
|
27
|
+
|
|
28
|
+
const sections: DocSection[] = ordered.map((label) => {
|
|
29
|
+
const items = (bySection.get(label) ?? []).slice().sort((a, b) => {
|
|
30
|
+
if (a.order !== b.order) return a.order - b.order
|
|
31
|
+
return a.title.localeCompare(b.title)
|
|
32
|
+
})
|
|
33
|
+
return { id: sectionIdOf(label), label, docs: items }
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const flat = sections.flatMap((s) => s.docs)
|
|
37
|
+
return { sections, flat }
|
|
38
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C6 — the docs-coherence check (state-machine.md Section 15c). Pure: given
|
|
3
|
+
* every parsed doc entry from `aeg-root/` and the model-backed surfaced-path
|
|
4
|
+
* allowlist (`modelBackedDocPaths`, passed in because deriving
|
|
5
|
+
* it requires reading doctrine, which this pure module never does), asserts:
|
|
6
|
+
* (a) every surfaced doc is reachable in the doc-nav tree the docs engine
|
|
7
|
+
* (`buildDocNav`) would build for the surfaced set;
|
|
8
|
+
* (b) no surfaced doc's `parent:` reference points at a doc that doesn't
|
|
9
|
+
* exist, or exists but isn't itself surfaced;
|
|
10
|
+
* (c) every relative `.md` link between two surfaced docs resolves to
|
|
11
|
+
* another surfaced doc.
|
|
12
|
+
*
|
|
13
|
+
* (a) and (b) mirror the real parent/child resolution
|
|
14
|
+
* `apps/vinaya/web/src/lib/docs/nest-doc-children.ts` performs when building
|
|
15
|
+
* Vinaya's live `/docs` nav (a Portal route since `vinaya-pages-v1` task 8;
|
|
16
|
+
* that file is read-only reference here, never imported): a doc whose
|
|
17
|
+
* `parent:` frontmatter points at
|
|
18
|
+
* a nonexistent (or excluded) slug is silently dropped from that nav's flat
|
|
19
|
+
* list — reachable neither at the top level nor as anyone's child. That
|
|
20
|
+
* silent-drop is the exact defect this check exists to catch.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { deriveTitle, type ParsedDoc } from './parse-doc'
|
|
24
|
+
import { isSurfacedDoc } from './surfaced-manifest'
|
|
25
|
+
import type { Doc, DocFrontmatter } from './types'
|
|
26
|
+
|
|
27
|
+
export type DocsCoherenceEntry = {
|
|
28
|
+
/** Path relative to `aeg-root/`, e.g. `roles/developer.md`. */
|
|
29
|
+
relPath: string
|
|
30
|
+
frontmatter: DocFrontmatter
|
|
31
|
+
body: string
|
|
32
|
+
firstH1?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type DocsCoherenceResult = { errors: string[]; notes: string[] }
|
|
36
|
+
|
|
37
|
+
function slugOf(relPath: string): string {
|
|
38
|
+
return relPath.replace(/\.md$/, '')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function relPathOf(slug: string): string {
|
|
42
|
+
return `${slug}.md`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function defaultSectionFor(relPath: string): string {
|
|
46
|
+
const segments = relPath.split('/')
|
|
47
|
+
return segments.length === 1 ? 'Overview' : (segments[0] ?? 'Overview')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toDoc(entry: DocsCoherenceEntry): Doc {
|
|
51
|
+
const slug = slugOf(entry.relPath)
|
|
52
|
+
const parsed: ParsedDoc = { frontmatter: entry.frontmatter, body: entry.body, firstH1: entry.firstH1 }
|
|
53
|
+
return {
|
|
54
|
+
slug,
|
|
55
|
+
title: deriveTitle(parsed, slug),
|
|
56
|
+
sidebarTitle: entry.frontmatter.sidebarTitle,
|
|
57
|
+
description: entry.frontmatter.description,
|
|
58
|
+
section: entry.frontmatter.section ?? defaultSectionFor(entry.relPath),
|
|
59
|
+
order: entry.frontmatter.order ?? 0,
|
|
60
|
+
href: `/docs/${slug}`,
|
|
61
|
+
filePath: `aeg-root/${entry.relPath}`,
|
|
62
|
+
parentSlug: entry.frontmatter.parent
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isReachable(doc: Doc, bySlug: Map<string, Doc>, seen: Set<string> = new Set()): boolean {
|
|
67
|
+
if (!doc.parentSlug) return true
|
|
68
|
+
if (seen.has(doc.slug)) return false // cycle guard
|
|
69
|
+
const parent = bySlug.get(doc.parentSlug)
|
|
70
|
+
if (!parent) return false
|
|
71
|
+
seen.add(doc.slug)
|
|
72
|
+
return isReachable(parent, bySlug, seen)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const MD_LINK_PATTERN = /\]\(([^)]+)\)/g
|
|
76
|
+
|
|
77
|
+
/** Resolves `target` relative to the directory of `fromRelPath`. Returns null if it escapes `aeg-root/`. */
|
|
78
|
+
function resolveRelativeMdLink(fromRelPath: string, target: string): string | null {
|
|
79
|
+
const fromDir = fromRelPath.includes('/') ? fromRelPath.slice(0, fromRelPath.lastIndexOf('/')) : ''
|
|
80
|
+
const combined = target.startsWith('/') ? target.slice(1) : fromDir ? `${fromDir}/${target}` : target
|
|
81
|
+
|
|
82
|
+
const stack: string[] = []
|
|
83
|
+
for (const part of combined.split('/')) {
|
|
84
|
+
if (part === '' || part === '.') continue
|
|
85
|
+
if (part === '..') {
|
|
86
|
+
if (stack.length === 0) return null
|
|
87
|
+
stack.pop()
|
|
88
|
+
} else {
|
|
89
|
+
stack.push(part)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return stack.join('/')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function evaluateDocsCoherence(
|
|
96
|
+
entries: DocsCoherenceEntry[],
|
|
97
|
+
surfacedPaths: ReadonlySet<string> = new Set()
|
|
98
|
+
): DocsCoherenceResult {
|
|
99
|
+
const errors: string[] = []
|
|
100
|
+
const notes: string[] = []
|
|
101
|
+
|
|
102
|
+
const surfacedEntries = entries.filter((e) => isSurfacedDoc(e.relPath, e.frontmatter, surfacedPaths))
|
|
103
|
+
const surfacedRelPaths = new Set(surfacedEntries.map((e) => e.relPath))
|
|
104
|
+
const docs = surfacedEntries.map(toDoc)
|
|
105
|
+
const bySlug = new Map(docs.map((d) => [d.slug, d]))
|
|
106
|
+
|
|
107
|
+
for (const doc of docs) {
|
|
108
|
+
if (!isReachable(doc, bySlug)) {
|
|
109
|
+
errors.push(`C6: surfaced doc "${relPathOf(doc.slug)}" is not reachable in the doc nav`)
|
|
110
|
+
}
|
|
111
|
+
if (doc.parentSlug && !bySlug.has(doc.parentSlug)) {
|
|
112
|
+
errors.push(
|
|
113
|
+
`C6: nav entry "${relPathOf(doc.slug)}" points at a non-existent/excluded doc "${relPathOf(doc.parentSlug)}"`
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const entry of surfacedEntries) {
|
|
119
|
+
for (const match of entry.body.matchAll(MD_LINK_PATTERN)) {
|
|
120
|
+
const raw = (match[1] ?? '').trim()
|
|
121
|
+
const target = (raw.split('#')[0] ?? '').trim()
|
|
122
|
+
if (!target) continue // pure same-doc anchor, e.g. `(#some-heading)`
|
|
123
|
+
if (/^https?:\/\//.test(target) || target.startsWith('mailto:')) continue
|
|
124
|
+
if (!target.endsWith('.md')) continue
|
|
125
|
+
|
|
126
|
+
const resolved = resolveRelativeMdLink(entry.relPath, target)
|
|
127
|
+
if (resolved === null) continue // escapes aeg-root/ — out of scope for this check
|
|
128
|
+
|
|
129
|
+
if (!surfacedRelPaths.has(resolved)) {
|
|
130
|
+
errors.push(`C6: link "${target}" in "${entry.relPath}" resolves to no surfaced doc`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { errors, notes }
|
|
136
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type { Doc, DocFrontmatter, DocNav, DocSection } from './types'
|
|
2
|
+
export { parseDocFrontmatter, deriveTitle, stripLeadingH1 } from './parse-doc'
|
|
3
|
+
export type { ParsedDoc } from './parse-doc'
|
|
4
|
+
export { buildDocNav } from './build-doc-nav'
|
|
5
|
+
export type { BuildDocNavOptions } from './build-doc-nav'
|
|
6
|
+
export { findDoc, getNextDoc, getPrevDoc } from './nav-helpers'
|
|
7
|
+
export { isSurfacedDoc, surfacedDocs, modelBackedDocPaths } from './surfaced-manifest'
|
|
8
|
+
export type { SurfacedManifestEntry } from './surfaced-manifest'
|
|
9
|
+
export { nodeDocRoute, nodeDocHref } from './node-route'
|
|
10
|
+
export type { NodeDocRoute } from './node-route'
|
|
11
|
+
export { legacyAnchorSlugs } from './legacy-anchors'
|
|
12
|
+
export { evaluateDocsCoherence } from './docs-coherence'
|
|
13
|
+
export type { DocsCoherenceEntry, DocsCoherenceResult } from './docs-coherence'
|
|
14
|
+
export {
|
|
15
|
+
ALLOWED_MECHANICS,
|
|
16
|
+
CONTRACT_BLOCKS,
|
|
17
|
+
countWords,
|
|
18
|
+
enforcementPublishedText,
|
|
19
|
+
evaluatePublishedProse,
|
|
20
|
+
extractShortVersion,
|
|
21
|
+
publishedDoctrineBody,
|
|
22
|
+
readabilityErrors,
|
|
23
|
+
REFERENCE_HEADING,
|
|
24
|
+
ROLE_BLOCKS,
|
|
25
|
+
SHORT_VERSION_HEADING,
|
|
26
|
+
SHORT_VERSION_MAX_WORDS,
|
|
27
|
+
SHORT_VERSION_MIN_WORDS
|
|
28
|
+
} from './published-prose'
|
|
29
|
+
export type { PublishedProseEntry, PublishedProseResult } from './published-prose'
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The old-slug → new-slug alias derivation for `/docs` gate/check anchors.
|
|
3
|
+
* `node-route.ts`'s `nodeSlug()` computes an anchor from a node's display
|
|
4
|
+
* form (G-code stripped, over-length names cut at a clause boundary), which
|
|
5
|
+
* for a handful of nodes now differs from the slug `diagram-model.ts`
|
|
6
|
+
* originally stamped into the node id. A reader who bookmarked, or a page
|
|
7
|
+
* that still links to, the old `#g1-implementation-exists`-style fragment
|
|
8
|
+
* must keep landing on the right section — this is the input to that alias.
|
|
9
|
+
*
|
|
10
|
+
* Derived, not hand-listed: the failure mode this replaces is exactly the
|
|
11
|
+
* one `node-route.ts`'s own header comment warns against for a second slug
|
|
12
|
+
* source. Zero I/O, additive export — takes an already-derived node, returns
|
|
13
|
+
* data, never reads a file (aeg-core purity, #372/#382/#506).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { DiagramNode } from '../diagram-model'
|
|
17
|
+
import { nodeDocRoute } from './node-route'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A doctrine cell's own text renamed outright (not a G-code strip, not an
|
|
21
|
+
* over-length cut) is the one case this file cannot derive. `node.id` is
|
|
22
|
+
* `slugify(row.action)` computed fresh from whatever `enforcement.md` says
|
|
23
|
+
* RIGHT NOW — by the time a rename lands, the old text is gone from the
|
|
24
|
+
* doctrine snapshot entirely, so there is nothing left in `node.id`/
|
|
25
|
+
* `node.label` for a derivation to diff against. This is not a second
|
|
26
|
+
* routing authority — `nodeSlug()`/`nodeDocRoute()` still compute every
|
|
27
|
+
* live anchor unconditionally; this map only remembers what one specific
|
|
28
|
+
* anchor used to be before an editor rewrote the cell it came from, and it
|
|
29
|
+
* grows by exactly one entry, once, at the moment of a rename. Keyed by the
|
|
30
|
+
* node id the rename produced. */
|
|
31
|
+
const RENAMED_CELL_ALIASES: Record<string, string> = {
|
|
32
|
+
'check:coherence-check': 'coherence-oracle'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The anchor slugs this node used to publish and must continue to answer
|
|
37
|
+
* to. `[]` when the node's canonical slug is unchanged — most nodes, since
|
|
38
|
+
* `nodeSlug()`'s cleanup is a no-op on an already-clean display form.
|
|
39
|
+
*/
|
|
40
|
+
export function legacyAnchorSlugs(node: DiagramNode): string[] {
|
|
41
|
+
const rawSlug = node.id.slice(node.kind.length + 1)
|
|
42
|
+
const canonicalSlug = nodeDocRoute(node)?.slug
|
|
43
|
+
const aliases = new Set<string>()
|
|
44
|
+
if (canonicalSlug && canonicalSlug !== rawSlug) aliases.add(rawSlug)
|
|
45
|
+
const renamed = RENAMED_CELL_ALIASES[node.id]
|
|
46
|
+
if (renamed) aliases.add(renamed)
|
|
47
|
+
return [...aliases]
|
|
48
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Doc, DocNav } from './types'
|
|
2
|
+
|
|
3
|
+
export function findDoc(nav: DocNav, slug: string): Doc | undefined {
|
|
4
|
+
return nav.flat.find((d) => d.slug === slug)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getNextDoc(nav: DocNav, slug: string): Doc | undefined {
|
|
8
|
+
const idx = nav.flat.findIndex((d) => d.slug === slug)
|
|
9
|
+
if (idx === -1 || idx === nav.flat.length - 1) return undefined
|
|
10
|
+
return nav.flat[idx + 1]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getPrevDoc(nav: DocNav, slug: string): Doc | undefined {
|
|
14
|
+
const idx = nav.flat.findIndex((d) => d.slug === slug)
|
|
15
|
+
if (idx <= 0) return undefined
|
|
16
|
+
return nav.flat[idx - 1]
|
|
17
|
+
}
|