@attalabs/vinaya 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/aeg-root/contracts/archivist-tranche-archivist.md +2 -2
- package/aeg-root/contracts/brief-developer.md +1 -0
- package/aeg-root/contracts/developer-reviewer.md +3 -3
- package/aeg-root/contracts/reviewer-archivist.md +3 -3
- package/aeg-root/enforcement.md +26 -20
- package/aeg-root/process.md +8 -4
- package/aeg-root/roles/archivist.md +2 -2
- package/aeg-root/roles/brief-author.md +9 -3
- package/aeg-root/roles/developer.md +63 -31
- package/aeg-root/roles/planner.md +1 -1
- package/aeg-root/roles/principal.md +14 -0
- package/aeg-root/roles/reviewer.md +32 -15
- package/aeg-root/roles/security.md +15 -5
- package/aeg-root/skills/aeg/SKILL.md +9 -5
- package/aeg-root/skills/aeg-roles/SKILL.md +2 -2
- package/aeg-root/skills/brief-authoring/SKILL.md +39 -27
- package/aeg-root/state-machine.md +9 -11
- package/aeg-root/templates/brief-template.md +6 -6
- package/aeg-root/templates/pr-report-template.md +10 -7
- package/aeg-root/tranche-model.md +6 -2
- package/dist/checks/bin/check-body-bare-digits.js +552 -139
- package/dist/checks/bin/check-branch-topology.js +603 -144
- package/dist/checks/bin/check-brief-shape.js +593 -142
- package/dist/checks/bin/check-changeset-coverage.js +935 -161
- package/dist/checks/bin/check-closes-n.js +603 -144
- package/dist/checks/bin/check-coherence.js +614 -147
- package/dist/checks/bin/check-dead-branch-push.js +552 -139
- package/dist/checks/bin/check-dispatch-readiness.js +612 -145
- package/dist/checks/bin/check-doc-coverage-push.js +689 -218
- package/dist/checks/bin/check-doc-coverage.js +689 -218
- package/dist/checks/bin/check-doctrine-no-procedures.js +4794 -0
- package/dist/checks/bin/check-doctrine-portability.js +684 -146
- package/dist/checks/bin/check-evidence-fresh.js +994 -174
- package/dist/checks/bin/check-exec-bits.js +4931 -0
- package/dist/checks/bin/check-first-push-dispatch.js +603 -144
- package/dist/checks/bin/check-issue-assignment.js +603 -144
- package/dist/checks/bin/check-main-branch-refusal.js +552 -139
- package/dist/checks/bin/check-no-disk-state.js +552 -139
- package/dist/checks/bin/check-pr-report-density.js +4461 -0
- package/dist/checks/bin/check-quoted-command.js +621 -155
- package/dist/checks/bin/check-reader-resolvable-prose.js +617 -154
- package/dist/checks/bin/check-registry-gates.js +595 -144
- package/dist/checks/bin/check-retired-vocabulary.js +617 -154
- package/dist/checks/bin/check-review-gate.js +670 -141
- package/dist/checks/bin/check-single-plan-pr.js +552 -139
- package/dist/checks/bin/check-test-plan.js +553 -140
- package/dist/checks/bin/check-token-collection-wired.js +552 -139
- package/dist/checks/bin/check-token-report.js +552 -139
- package/dist/checks/bin/check-workspace-escape.js +985 -145
- package/dist/index.js +8500 -5518
- package/package.json +1 -1
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/actions.ts +173 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/anchored-region.ts +113 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/archive-task.ts +206 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/baseline-capture.ts +65 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/blast-radius-domains.ts +192 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/branch-topology-gate.ts +85 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-validation.ts +511 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/claude-code-transcript.ts +460 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/coherence-checks.ts +787 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-audit.ts +55 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-guard.ts +77 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-section7.ts +66 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-tranche.ts +94 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/diagram-model.ts +372 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/direct-main-push.ts +39 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dispatch-gate.ts +224 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doc-owners.ts +314 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/build-doc-nav.ts +38 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/docs-coherence.ts +136 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/index.ts +29 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/legacy-anchors.ts +48 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/nav-helpers.ts +17 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/node-route.ts +134 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/parse-doc.ts +43 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/published-prose.ts +306 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/surfaced-manifest.ts +89 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/types.ts +33 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-portability.ts +170 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-source.ts +23 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/ensure-label.ts +28 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/file-classify.ts +51 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/first-push-dispatch-gate.ts +80 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/gate-audience.ts +168 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/index.ts +301 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-assignment.ts +102 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-validation.ts +746 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/leftover-detection.ts +68 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/local-anchor-coverage.ts +245 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/main-branch-refusal.ts +64 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/manifest-validity.ts +81 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/markdown-table.ts +76 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/metering-io-guard.ts +119 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/milestone-validation.ts +240 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/no-disk-state.ts +50 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-ledger.ts +77 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-registry.ts +105 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-token-report.ts +165 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-tranche.ts +183 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-tier.ts +81 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/premise-check.ts +125 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/quoted-command.ts +275 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/reader-resolvable-prose.ts +320 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-checks.ts +255 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-parse.ts +164 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-scaffold.ts +216 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/report-tokens.ts +261 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/retired-vocabulary.ts +163 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-gate.ts +269 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/single-plan-pr.ts +73 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-machine-model.ts +269 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-source.ts +14 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/status-block.ts +8 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/sum-ledger.ts +26 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/symbol-collisions.ts +78 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-gate.ts +104 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-section.ts +65 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/types.ts +174 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/verdict-extraction.ts +140 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/vocabulary-citation.ts +114 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/waiver-label.ts +52 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/workspace-escape.ts +172 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__02a-3g8._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/1q96_modules_@clerk_nextjs_dist_esm_app-router_client_keyless-creator-reader_0lom2js.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__069-_41._.js → [root-of-the-server]__1mcitst._.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{37hnluhzj9bei.js → 0jer_drkl1e4r.js} +4 -4
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{267o-tsrnuwnv.js → 20x-t0a0v129z.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1hufpuody6vjv.js → 2ou_81ahqw31e.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- /package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -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
|
+
}
|