@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,183 @@
|
|
|
1
|
+
import { readMarkdownTable } from './parse-registry'
|
|
2
|
+
import type { Tranche, Lifecycle, Task } from './types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse `aeg-root/tranches/<name>.md` into a typed `Tranche`.
|
|
6
|
+
*
|
|
7
|
+
* Captures:
|
|
8
|
+
* - the tranche's name (from `# Tranche: <name> — <timeframe>`; the
|
|
9
|
+
* superseded `# Iteration:` spelling is still read, see `parseName`)
|
|
10
|
+
* - the lifecycle marker (`Lifecycle: active|complete`, defaulting to
|
|
11
|
+
* `'active'` when absent — pre-§11 files have no marker)
|
|
12
|
+
* - the first goal paragraph
|
|
13
|
+
* - the `## Tasks (topology)` table rows
|
|
14
|
+
* - per-task rationale blocks (raw markdown, captured verbatim)
|
|
15
|
+
* - an optional `## Backlog` section as bullets
|
|
16
|
+
*
|
|
17
|
+
* Real tranche files include narrative references to dropped task ids in
|
|
18
|
+
* prose (e.g. a "Task 3a — removed" note). The parser ignores those — only
|
|
19
|
+
* rows present in the topology table become tasks.
|
|
20
|
+
*/
|
|
21
|
+
export function parseTranche(md: string): Tranche {
|
|
22
|
+
return {
|
|
23
|
+
name: parseName(md),
|
|
24
|
+
lifecycle: parseLifecycle(md),
|
|
25
|
+
goal: parseGoal(md),
|
|
26
|
+
tasks: parseTasks(md),
|
|
27
|
+
backlog: parseBacklog(md)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseName(md: string): string {
|
|
32
|
+
// The H1 is `# Tranche: <slug> — <timeframe>`. The slug may contain
|
|
33
|
+
// hyphens (e.g. `some-tranche-name`, `another-v1`), so capture the first
|
|
34
|
+
// non-whitespace run — the space before the em-dash is the delimiter.
|
|
35
|
+
//
|
|
36
|
+
// `Iteration:` is accepted as the superseded spelling, permanently. This is
|
|
37
|
+
// a READER of files this repo does not necessarily own: content pulled out
|
|
38
|
+
// of git history predates the rename and cannot be rewritten, and an
|
|
39
|
+
// adopter's own topology files are theirs, not ours to migrate. Refusing
|
|
40
|
+
// the old marker would not fail loudly — `parseName` returns `''`, which
|
|
41
|
+
// reads downstream as a nameless tranche. The writer side is unaffected:
|
|
42
|
+
// every file this repo emits or archives carries `# Tranche:`.
|
|
43
|
+
const m = md.match(/^#\s+(?:Tranche|Iteration):\s+(\S+)/m)
|
|
44
|
+
return m?.[1] ? m[1].trim() : ''
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseLifecycle(md: string): Lifecycle {
|
|
48
|
+
// `Lifecycle: active` or `Lifecycle: complete`. The label may be wrapped in
|
|
49
|
+
// bold (`**Lifecycle:** active`); the closing `**` sits after the colon.
|
|
50
|
+
const m = md.match(/^\s*(?:\*\*)?Lifecycle\s*:(?:\*\*)?\s*(active|complete)\b/im)
|
|
51
|
+
return m?.[1] ? (m[1].toLowerCase() as Lifecycle) : 'active'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseGoal(md: string): string {
|
|
55
|
+
// Match `Goal (…): <text>` to the end of the paragraph.
|
|
56
|
+
// The live files use various subtitles ("execution, not roadmap-why",
|
|
57
|
+
// "execution, not product-why"); strip everything up to the colon and
|
|
58
|
+
// capture the first paragraph that follows.
|
|
59
|
+
const lines = md.split(/\r?\n/)
|
|
60
|
+
const buf: string[] = []
|
|
61
|
+
let inGoal = false
|
|
62
|
+
for (const raw of lines) {
|
|
63
|
+
const line = stripBoldWrappers(raw)
|
|
64
|
+
if (!inGoal) {
|
|
65
|
+
const m = line.match(/^Goal\b[^:]*:\s*(.*)$/i)
|
|
66
|
+
if (m) {
|
|
67
|
+
inGoal = true
|
|
68
|
+
const rest = (m[1] ?? '').trim()
|
|
69
|
+
if (rest) buf.push(rest)
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
if (line.trim() === '') break
|
|
73
|
+
// Stop at the next markdown heading or HR.
|
|
74
|
+
if (/^#{1,6}\s/.test(line) || /^---+\s*$/.test(line)) break
|
|
75
|
+
buf.push(line.trim())
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return buf.join(' ').trim()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function stripBoldWrappers(line: string): string {
|
|
82
|
+
// Remove bold markers around `Goal …:` or `Lifecycle:` style lines so the
|
|
83
|
+
// greedy regexes above work on either `**Goal:**` or plain `Goal:`.
|
|
84
|
+
return line.replace(/\*\*/g, '')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseTasks(md: string): Task[] {
|
|
88
|
+
const rows = readMarkdownTable(md, /^##\s+Tasks\b/i)
|
|
89
|
+
const rationales = extractRationales(md)
|
|
90
|
+
const tasks: Task[] = []
|
|
91
|
+
for (const row of rows) {
|
|
92
|
+
// Expected 6 columns: # | Task | Issue | Project(s) | Depends-on | Conflicts-with.
|
|
93
|
+
if (row.length < 6) continue
|
|
94
|
+
const id = (row[0] ?? '').trim()
|
|
95
|
+
if (!id) continue
|
|
96
|
+
tasks.push({
|
|
97
|
+
id,
|
|
98
|
+
title: (row[1] ?? '').trim(),
|
|
99
|
+
issue: parseIssueCell(row[2] ?? ''),
|
|
100
|
+
projects: parseCsvCell(row[3] ?? ''),
|
|
101
|
+
dependsOn: parseEdgeCell(row[4] ?? ''),
|
|
102
|
+
conflictsWith: parseEdgeCell(row[5] ?? ''),
|
|
103
|
+
rationaleMarkdown: rationales.get(id) ?? ''
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
return tasks
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseIssueCell(cell: string): number | null {
|
|
110
|
+
const trimmed = stripBackticks(cell).trim()
|
|
111
|
+
if (!trimmed) return null
|
|
112
|
+
if (isEmDashOrDash(trimmed)) return null
|
|
113
|
+
const m = trimmed.match(/#?(\d+)/)
|
|
114
|
+
return m ? Number(m[1]) : null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseCsvCell(cell: string): string[] {
|
|
118
|
+
const cleaned = stripBackticks(cell).trim()
|
|
119
|
+
if (!cleaned || isEmDashOrDash(cleaned)) return []
|
|
120
|
+
return cleaned
|
|
121
|
+
.split(',')
|
|
122
|
+
.map((s) => s.trim())
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseEdgeCell(cell: string): string[] {
|
|
127
|
+
return parseCsvCell(cell)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isEmDashOrDash(s: string): boolean {
|
|
131
|
+
const t = s.trim()
|
|
132
|
+
return t === '—' || t === '-' || t === '–'
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function stripBackticks(s: string): string {
|
|
136
|
+
return s.replace(/`/g, '')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Capture each `### Task <id> — …` block as raw markdown — from the heading
|
|
141
|
+
* through the line before the next `### ` / `## ` / `# ` / end-of-file.
|
|
142
|
+
* The id may be `1`, `7a`, `7b`, etc. Per-task rationale lives inline in the
|
|
143
|
+
* tranche file (see §4 template); the planner's rationale also lives in the
|
|
144
|
+
* Issue body, but the inline copy is what `parseTranche` exposes.
|
|
145
|
+
*/
|
|
146
|
+
function extractRationales(md: string): Map<string, string> {
|
|
147
|
+
const out = new Map<string, string>()
|
|
148
|
+
const lines = md.split(/\r?\n/)
|
|
149
|
+
let i = 0
|
|
150
|
+
while (i < lines.length) {
|
|
151
|
+
const m = (lines[i] ?? '').match(/^###\s+Task\s+(\S+)\s+—/)
|
|
152
|
+
if (!m?.[1]) {
|
|
153
|
+
i++
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
const id = m[1]
|
|
157
|
+
const start = i
|
|
158
|
+
i++
|
|
159
|
+
while (i < lines.length && !/^#{1,3}\s/.test(lines[i] ?? '')) i++
|
|
160
|
+
out.set(id, lines.slice(start, i).join('\n').trimEnd())
|
|
161
|
+
}
|
|
162
|
+
return out
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseBacklog(md: string): string[] {
|
|
166
|
+
const lines = md.split(/\r?\n/)
|
|
167
|
+
let i = 0
|
|
168
|
+
for (; i < lines.length; i++) {
|
|
169
|
+
if (/^##\s+Backlog\b/i.test(lines[i] ?? '')) {
|
|
170
|
+
i++
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (i >= lines.length) return []
|
|
175
|
+
const bullets: string[] = []
|
|
176
|
+
for (; i < lines.length; i++) {
|
|
177
|
+
const line = lines[i] ?? ''
|
|
178
|
+
if (/^#{1,3}\s/.test(line)) break
|
|
179
|
+
const m = line.match(/^\s*[-*+]\s+(.*)$/)
|
|
180
|
+
if (m?.[1]) bullets.push(m[1].trim())
|
|
181
|
+
}
|
|
182
|
+
return bullets
|
|
183
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR-report density (aeg-governance-hardening, dogfooding finding 2026-09-02).
|
|
3
|
+
* Pure — no `fs`, no `fetch`, no `process.env`.
|
|
4
|
+
*
|
|
5
|
+
* `aeg-root/roles/developer.md` § "PR body — canonical form" and
|
|
6
|
+
* `aeg-root/templates/pr-report-template.md` both require `## Summary` and
|
|
7
|
+
* `## Scope` to be exactly one paragraph — narrative root-cause detail
|
|
8
|
+
* belongs in a section of its own below the canonical four, a commit
|
|
9
|
+
* message, or the changeset, never padded into the report itself and never
|
|
10
|
+
* hand-typed into the AEG:EVIDENCE block. Nothing enforced that rule
|
|
11
|
+
* mechanically:
|
|
12
|
+
* confirmed live, three real PRs (`atta-labs/vinaya#343`, `#346`, `#354`)
|
|
13
|
+
* shipped multi-paragraph Summary/Scope sections and nothing caught it,
|
|
14
|
+
* neither at `vinaya pr create`/`pr edit` time nor in CI. Longer, denser
|
|
15
|
+
* prose is also strictly more surface for every other body-text gate
|
|
16
|
+
* (`body-bare-digits`, `reader-resolvable-prose`'s glossary rule) — the same
|
|
17
|
+
* class of gap `#341` hit in code, just in prose instead.
|
|
18
|
+
*
|
|
19
|
+
* Deterministic and structural only, matching `brief-validation.ts`'s own
|
|
20
|
+
* "presence-only" philosophy: this counts blank-line-delimited text blocks,
|
|
21
|
+
* never judges whether the prose itself is good.
|
|
22
|
+
*
|
|
23
|
+
* The rule is literal — one paragraph, full stop — so a `### subsection`, a
|
|
24
|
+
* bullet list, or a markdown table under Summary/Scope all fail too, same as
|
|
25
|
+
* a second prose paragraph would (confirmed by probe, PR review on #358).
|
|
26
|
+
* That is intentional, not an oversight: `developer.md`'s own escape hatch
|
|
27
|
+
* for exactly this case is "Add anything you want beneath the four
|
|
28
|
+
* sections" — structured detail belongs in a section of its own below
|
|
29
|
+
* Summary/Scope/Test-plan/Evidence, not folded into one of the four. A
|
|
30
|
+
* multi-line blockquote is the one shape this rule does NOT split on (its
|
|
31
|
+
* `\n>\n` continuation lines are never blank) — but only when the
|
|
32
|
+
* blockquote IS the section, start to finish: prose before or after it is
|
|
33
|
+
* still a separate blank-line-delimited block, so "intro sentence, then a
|
|
34
|
+
* blockquote aside, then a closing sentence" is three blocks and still
|
|
35
|
+
* fails (confirmed by probe, PR review on #358 — this is not the
|
|
36
|
+
* "structured aside alongside prose" shape it can look like at a glance).
|
|
37
|
+
* The exemption only covers a section whose entire content, quoted in
|
|
38
|
+
* full, is the one thing being said.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { anchoredRegionBounds, stripCode } from './anchored-region'
|
|
42
|
+
|
|
43
|
+
export type DensityResult = { status: 'pass' | 'fail'; errors: string[] }
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The named `## <heading>` section's raw body text — from just after the
|
|
47
|
+
* heading line to the next `##`-or-shallower heading, or end of body.
|
|
48
|
+
* `null` when the heading itself is absent (a different check's job to
|
|
49
|
+
* require it — this one is silent on presence, per the module's scope).
|
|
50
|
+
*/
|
|
51
|
+
function headingSectionBody(prBody: string, heading: string): string | null {
|
|
52
|
+
const headingRe = new RegExp(`^##\\s+${heading}\\s*$`, 'im')
|
|
53
|
+
const m = headingRe.exec(prBody)
|
|
54
|
+
if (!m) return null
|
|
55
|
+
const start = m.index + m[0].length
|
|
56
|
+
const rest = prBody.slice(start)
|
|
57
|
+
const nextHeading = /^#{1,2}\s/m.exec(rest)
|
|
58
|
+
return nextHeading ? rest.slice(0, nextHeading.index) : rest
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* `headingSectionBody`'s text with every well-formed `AEG:*` anchor block removed
|
|
63
|
+
* outright (not just unwrapped) — an anchored field (Scope's trailing
|
|
64
|
+
* `AEG:TIER` block, in the canonical template) is a field, not a second
|
|
65
|
+
* prose paragraph, and must not count as one. Reuses `anchoredRegionBounds`
|
|
66
|
+
* (the same span `vinaya pr report --write` targets) rather than a second,
|
|
67
|
+
* drifting regex for "what an anchor block looks like."
|
|
68
|
+
*/
|
|
69
|
+
function stripAnchorBlocks(sectionText: string): string {
|
|
70
|
+
let result = sectionText
|
|
71
|
+
for (const field of ['CLOSES', 'PROJECT', 'TIER', 'PREMISE', 'TEST-PLAN', 'EVIDENCE'] as const) {
|
|
72
|
+
for (;;) {
|
|
73
|
+
const bounds = anchoredRegionBounds(result, field)
|
|
74
|
+
if (!bounds) break
|
|
75
|
+
result = result.slice(0, bounds.outerStart) + result.slice(bounds.outerEnd)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Non-empty, blank-line-delimited text blocks, code-blind (a fenced example inside the section is not a second paragraph). */
|
|
82
|
+
function paragraphCount(sectionText: string): number {
|
|
83
|
+
const stripped = stripCode(stripAnchorBlocks(sectionText)).trim()
|
|
84
|
+
if (stripped.length === 0) return 0
|
|
85
|
+
return stripped
|
|
86
|
+
.split(/\n[ \t]*\n/)
|
|
87
|
+
.map((block) => block.trim())
|
|
88
|
+
.filter((block) => block.length > 0).length
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function checkSectionDensity(prBody: string, heading: string): DensityResult {
|
|
92
|
+
const body = headingSectionBody(prBody, heading)
|
|
93
|
+
if (body === null) return { status: 'pass', errors: [] }
|
|
94
|
+
const count = paragraphCount(body)
|
|
95
|
+
if (count <= 1) return { status: 'pass', errors: [] }
|
|
96
|
+
return {
|
|
97
|
+
status: 'fail',
|
|
98
|
+
errors: [
|
|
99
|
+
`pr-report-density ${heading}: "## ${heading}" holds ${count} paragraphs — the canonical PR-report form (aeg-root/roles/developer.md § PR body) requires exactly one. Collapse it to one paragraph; move the rest into a section of your own below the canonical four ("Add anything you want beneath the four sections", developer.md), a commit message, or the changeset — never into the AEG:EVIDENCE block, which is emitted only, never hand-typed.`
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function checkSummaryDensity(prBody: string): DensityResult {
|
|
105
|
+
return checkSectionDensity(prBody, 'Summary')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function checkScopeDensity(prBody: string): DensityResult {
|
|
109
|
+
return checkSectionDensity(prBody, 'Scope')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Aggregates both section checks — one error line per over-dense section. */
|
|
113
|
+
export function checkPrReportDensity(prBody: string): { errors: string[] } {
|
|
114
|
+
const results = [checkSummaryDensity(prBody), checkScopeDensity(prBody)]
|
|
115
|
+
return { errors: results.flatMap((r) => r.errors) }
|
|
116
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR tier derivation and the docs-gate override check. Pure — takes the diff,
|
|
3
|
+
* PR body, and env-derived strings as explicit parameters. The original
|
|
4
|
+
* `scripts/verify-docs.ts` read `process.env.PR_BODY` / `OVERRIDE_DOCS` /
|
|
5
|
+
* `PR_LABELS` directly inside these functions; that hidden I/O is removed
|
|
6
|
+
* here — the caller (the CLI shim) reads env once and passes the values in.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { hasLabel, label } from '@attalabs/aeg-forge-state'
|
|
10
|
+
import { anchoredRegion } from './anchored-region'
|
|
11
|
+
import { isDocFile, isSpecFile } from './file-classify'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Derive a tier from the changed-file list when no `Tier:` field is in the PR body.
|
|
15
|
+
*
|
|
16
|
+
* Rules (in priority order):
|
|
17
|
+
* 1. Spec or doc file in diff → Tier 1
|
|
18
|
+
* 2. Otherwise (code/config…) → Tier 0
|
|
19
|
+
*
|
|
20
|
+
* A frozen archive carries no tier signal: nothing reads it, so touching one
|
|
21
|
+
* says nothing about a change's impact.
|
|
22
|
+
*/
|
|
23
|
+
export function deriveTierFromDiff(changed: string[]): 0 | 1 {
|
|
24
|
+
if (changed.some((p) => isSpecFile(p) || isDocFile(p))) return 1
|
|
25
|
+
return 0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read the `Tier:` field from the PR body.
|
|
30
|
+
*
|
|
31
|
+
* Tolerates the three markdown shapes the field appears in:
|
|
32
|
+
* - plain: `Tier: 3`
|
|
33
|
+
* - bold colon: `**Tier:** 3` (the `**` wraps `Tier:` including the colon)
|
|
34
|
+
* - bold label: `**Tier**: 3` (the `**` wraps only `Tier`)
|
|
35
|
+
*
|
|
36
|
+
* The field may appear inline in a metadata line (e.g.
|
|
37
|
+
* `Tranche: x · Task: 1 · **Tier:** 3 · Project: y`), so it is NOT anchored
|
|
38
|
+
* to line-start. Returns null when no Tier field is present at all — the caller
|
|
39
|
+
* decides what a missing tier means (PR mode treats it as an explicit error,
|
|
40
|
+
* NOT a silent default).
|
|
41
|
+
*
|
|
42
|
+
* When the body carries an `AEG:TIER` anchor pair (`anchored-region.ts`,
|
|
43
|
+
* task 30), the same regex runs exclusively inside that pair — a `Tier:`
|
|
44
|
+
* mention anywhere else (a pasted reference brief, a quoted example) is
|
|
45
|
+
* ignored. Bodies without the pair parse exactly as before.
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* The exact `Tier:` field grammar, exported so a consumer that needs the
|
|
49
|
+
* MATCH itself (not just the parsed number) — `body-bare-digits`, which
|
|
50
|
+
* needs to know precisely which substring is the field's real value so it
|
|
51
|
+
* can exempt exactly that and nothing appended after it — reuses this one
|
|
52
|
+
* definition rather than a second regex that could silently drift from it.
|
|
53
|
+
* An optional bold-open, the word Tier, an optional bold-close, a colon, an
|
|
54
|
+
* optional bold-close (covers `**Tier:**`), optional space, then the digit.
|
|
55
|
+
*/
|
|
56
|
+
export const TIER_FIELD = /(\*\*)?\s*Tier\s*(\*\*)?\s*:\s*(\*\*)?\s*([013])\b/i
|
|
57
|
+
|
|
58
|
+
export function readTierFromPrBody(prBody: string): 0 | 1 | 3 | null {
|
|
59
|
+
const searchIn = anchoredRegion(prBody, 'TIER') ?? prBody
|
|
60
|
+
const m = searchIn.match(TIER_FIELD)
|
|
61
|
+
if (!m) return null
|
|
62
|
+
const t = Number(m[4])
|
|
63
|
+
return t === 0 || t === 1 || t === 3 ? (t as 0 | 1 | 3) : null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The body token that activates the override — the label name in brackets, so
|
|
68
|
+
* the two spellings cannot drift. Built from the code-owned vocabulary rather
|
|
69
|
+
* than written as a literal (#614): `override:docs` was the one §14 system
|
|
70
|
+
* label the namespace migration missed, precisely because it lived here as a
|
|
71
|
+
* bare string that no label-family grep looked for.
|
|
72
|
+
*/
|
|
73
|
+
const OVERRIDE_BODY_TOKEN = `[${label('override-docs')}]`
|
|
74
|
+
|
|
75
|
+
export function overrideActive(opts: { overrideDocsEnv?: string; prLabels?: string; prBody?: string }): boolean {
|
|
76
|
+
if (opts.overrideDocsEnv === '1') return true
|
|
77
|
+
const labels = (opts.prLabels || '').split(',').map((s) => s.trim())
|
|
78
|
+
if (hasLabel('override-docs', labels)) return true
|
|
79
|
+
if ((opts.prBody || '').includes(OVERRIDE_BODY_TOKEN)) return true
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Premise-pinning grammar (aeg-governance-hardening task 11, #324). Pure —
|
|
3
|
+
* no `fs`; file content is injected via `fileReader` so the checker stays
|
|
4
|
+
* testable and CWD-independent.
|
|
5
|
+
*
|
|
6
|
+
* A brief/PR body's `Premise:` block pins concrete, checkable facts about the
|
|
7
|
+
* code surface at authoring time — e.g. "this function still exists", "this
|
|
8
|
+
* constant is still absent". `verify-dispatch --premise <body-file>`
|
|
9
|
+
* re-asserts every pin immediately before Step 0: a premise that no longer
|
|
10
|
+
* holds means the surface moved since the brief was written, and the
|
|
11
|
+
* Developer should stop and re-dig rather than execute against a stale
|
|
12
|
+
* mental model (the exact failure of aeg-governance-hardening's live-fire
|
|
13
|
+
* #3 — two briefs described a target architecture a later, uncited migration
|
|
14
|
+
* had already superseded).
|
|
15
|
+
*
|
|
16
|
+
* Deliberately minimal — exactly three assertion kinds. This is a pin
|
|
17
|
+
* format, not a DSL; resist adding more.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { createHash } from 'node:crypto'
|
|
21
|
+
import { anchoredRegion } from './anchored-region'
|
|
22
|
+
|
|
23
|
+
export type PremiseAssertion =
|
|
24
|
+
| { kind: 'contains'; path: string; value: string }
|
|
25
|
+
| { kind: 'absent'; path: string; value: string }
|
|
26
|
+
| { kind: 'sha256'; path: string; value: string }
|
|
27
|
+
|
|
28
|
+
const ASSERTION_KINDS = new Set(['contains', 'absent', 'sha256'])
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* True for the header line in either serialization the rest of the brief
|
|
32
|
+
* grammar uses (`brief-validation.ts`, `issue-validation.ts`): `**Premise:**`
|
|
33
|
+
* bold-inline or `### Premise` heading. Strips `*`/`#` markup first rather
|
|
34
|
+
* than trying to enumerate every marker placement — `**Premise:**` closes
|
|
35
|
+
* its bold markers *after* the colon, `**Premise**:` closes them *before*.
|
|
36
|
+
*/
|
|
37
|
+
function isPremiseHeader(line: string): boolean {
|
|
38
|
+
const stripped = line.replace(/[*#]/g, '').trim()
|
|
39
|
+
return /^premise\s*:?$/i.test(stripped)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Matches one bullet line: `- <path> <kind>: <value>`. */
|
|
43
|
+
const PREMISE_LINE = /^[-*]\s*(\S+)\s+(contains|absent|sha256)\s*:\s*(.+)$/i
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse every `- <path> <kind>: <value>` bullet under the `Premise:` header.
|
|
47
|
+
* Malformed bullets (wrong kind, missing colon) are silently skipped — this
|
|
48
|
+
* mirrors `parsePremiseBlock`'s sibling parsers' presence-only philosophy;
|
|
49
|
+
* the caller decides whether zero assertions is itself a failure
|
|
50
|
+
* (`checkPremiseCoverage`). Stops the block at the first blank line or the
|
|
51
|
+
* first non-bullet line after the header.
|
|
52
|
+
*
|
|
53
|
+
* When the body carries an `AEG:PREMISE` anchor pair (`anchored-region.ts`,
|
|
54
|
+
* task 30), the block is parsed exclusively inside that pair — the pair must
|
|
55
|
+
* contain the `**Premise:**` header line and its bullets; a premise-shaped
|
|
56
|
+
* block anywhere else in the body is ignored. Bodies without the pair parse
|
|
57
|
+
* exactly as before.
|
|
58
|
+
*/
|
|
59
|
+
export function parsePremiseBlock(prBody: string): PremiseAssertion[] {
|
|
60
|
+
const searchIn = anchoredRegion(prBody, 'PREMISE') ?? prBody
|
|
61
|
+
const lines = searchIn.split(/\r?\n/)
|
|
62
|
+
const assertions: PremiseAssertion[] = []
|
|
63
|
+
let inBlock = false
|
|
64
|
+
|
|
65
|
+
for (const raw of lines) {
|
|
66
|
+
const line = raw.trim()
|
|
67
|
+
if (!inBlock) {
|
|
68
|
+
if (isPremiseHeader(line)) inBlock = true
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
if (line === '') break
|
|
72
|
+
const m = line.match(PREMISE_LINE)
|
|
73
|
+
if (!m) break
|
|
74
|
+
const [, path, kindRaw, value] = m
|
|
75
|
+
const kind = (kindRaw as string).toLowerCase()
|
|
76
|
+
if (!ASSERTION_KINDS.has(kind)) continue
|
|
77
|
+
assertions.push({ kind, path: path as string, value: (value as string).trim() } as PremiseAssertion)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return assertions
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The `Premise:` block's own raw text (header line through its last bullet),
|
|
85
|
+
* mirroring `parsePremiseBlock`'s exact same header/bullet/stop rules so the
|
|
86
|
+
* two can never disagree about where the block ends. Returns `null` when no
|
|
87
|
+
* header is found. Used by `brief-validation.ts`'s `checkNoUnpinnedCodeClaims`
|
|
88
|
+
* to exclude the block's own bullets (a pin's path is a legitimate file
|
|
89
|
+
* reference, not an unpinned prose claim) — reuses this parser's block
|
|
90
|
+
* boundaries rather than a second one.
|
|
91
|
+
*/
|
|
92
|
+
export function premiseBlockText(prBody: string): string | null {
|
|
93
|
+
const searchIn = anchoredRegion(prBody, 'PREMISE') ?? prBody
|
|
94
|
+
const lines = searchIn.split(/\r?\n/)
|
|
95
|
+
let startIdx = -1
|
|
96
|
+
let endIdxExclusive = lines.length
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < lines.length; i++) {
|
|
99
|
+
const line = (lines[i] as string).trim()
|
|
100
|
+
if (startIdx === -1) {
|
|
101
|
+
if (isPremiseHeader(line)) startIdx = i
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
if (line === '') {
|
|
105
|
+
endIdxExclusive = i
|
|
106
|
+
break
|
|
107
|
+
}
|
|
108
|
+
if (!PREMISE_LINE.test(line)) {
|
|
109
|
+
endIdxExclusive = i
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (startIdx === -1) return null
|
|
115
|
+
return lines.slice(startIdx, endIdxExclusive).join('\n')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type PremiseCheckResult = { pass: boolean; failures: string[] }
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Re-assert every parsed premise against the current file content, injected
|
|
122
|
+
* via `fileReader` (returns `null` when the path does not exist). One
|
|
123
|
+
* failure line per broken pin, naming the exact path/kind/value so the
|
|
124
|
+
* Developer can see precisely what moved.
|
|
125
|
+
*/
|
|
126
|
+
export function checkPremises(
|
|
127
|
+
assertions: PremiseAssertion[],
|
|
128
|
+
fileReader: (path: string) => string | null
|
|
129
|
+
): PremiseCheckResult {
|
|
130
|
+
const failures: string[] = []
|
|
131
|
+
|
|
132
|
+
for (const a of assertions) {
|
|
133
|
+
const content = fileReader(a.path)
|
|
134
|
+
if (content === null) {
|
|
135
|
+
failures.push(
|
|
136
|
+
`premise-check: ${a.path} does not exist on disk (premise asserted \`${a.kind}: ${a.value}\`) — the surface moved since this brief was authored; re-dig before proceeding.`
|
|
137
|
+
)
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (a.kind === 'contains' && !content.includes(a.value)) {
|
|
142
|
+
failures.push(
|
|
143
|
+
`premise-check: ${a.path} no longer contains "${a.value}" — the premise this brief pinned has moved; re-dig before proceeding.`
|
|
144
|
+
)
|
|
145
|
+
} else if (a.kind === 'absent' && content.includes(a.value)) {
|
|
146
|
+
failures.push(
|
|
147
|
+
`premise-check: ${a.path} now contains "${a.value}", but the brief pinned it absent — the premise has moved; re-dig before proceeding.`
|
|
148
|
+
)
|
|
149
|
+
} else if (a.kind === 'sha256') {
|
|
150
|
+
const actual = createHash('sha256').update(content).digest('hex')
|
|
151
|
+
if (actual !== a.value.toLowerCase()) {
|
|
152
|
+
failures.push(
|
|
153
|
+
`premise-check: ${a.path} sha256 mismatch (brief pinned ${a.value}, file is now ${actual}) — the file changed since this brief was authored; re-dig before proceeding.`
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { pass: failures.length === 0, failures }
|
|
160
|
+
}
|