@attalabs/vinaya 0.23.0 → 0.24.1
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 +1 -0
- package/aeg-root/enforcement.md +1 -1
- package/aeg-root/roles/developer.md +1 -1
- package/dist/checks/bin/check-body-bare-digits.js +7 -1
- package/dist/checks/bin/check-branch-topology.js +27 -1
- package/dist/checks/bin/check-brief-shape.js +7 -1
- package/dist/checks/bin/check-changeset-coverage.js +19 -1
- package/dist/checks/bin/check-closes-n.js +27 -1
- package/dist/checks/bin/check-coherence.js +27 -1
- package/dist/checks/bin/check-dead-branch-push.js +7 -1
- package/dist/checks/bin/check-dispatch-readiness.js +27 -1
- package/dist/checks/bin/check-doc-coverage-push.js +7 -1
- package/dist/checks/bin/check-doc-coverage.js +7 -1
- package/dist/checks/bin/check-doctrine-no-procedures.js +7 -1
- package/dist/checks/bin/check-doctrine-portability.js +7 -1
- package/dist/checks/bin/check-evidence-fresh.js +7 -1
- package/dist/checks/bin/check-exec-bits.js +7 -1
- package/dist/checks/bin/check-first-push-dispatch.js +27 -1
- package/dist/checks/bin/check-issue-assignment.js +27 -1
- package/dist/checks/bin/check-main-branch-refusal.js +13 -2
- package/dist/checks/bin/check-no-disk-state.js +7 -1
- package/dist/checks/bin/check-pr-report-density.js +7 -1
- package/dist/checks/bin/check-quoted-command.js +38 -12
- package/dist/checks/bin/check-reader-resolvable-prose.js +38 -12
- package/dist/checks/bin/check-registry-gates.js +8 -2
- package/dist/checks/bin/check-retired-vocabulary.js +38 -12
- package/dist/checks/bin/check-review-gate.js +7 -1
- package/dist/checks/bin/check-single-plan-pr.js +7 -1
- package/dist/checks/bin/check-test-plan.js +7 -1
- package/dist/checks/bin/check-token-collection-wired.js +7 -1
- package/dist/checks/bin/check-token-report.js +7 -1
- package/dist/checks/bin/check-workspace-escape.js +17 -3
- package/dist/index.js +405 -98
- package/package.json +2 -2
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/anchored-region.ts +9 -4
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/blast-radius-domains.ts +7 -1
- 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 +405 -4
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/coherence-checks.ts +19 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/consumer-enumeration.ts +75 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dispatch-gate.ts +44 -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 +128 -5
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/gate-audience.ts +3 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/index.ts +25 -3
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-token-report.ts +17 -5
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-report-density.ts +116 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/premise-check.ts +35 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/quoted-command.ts +3 -3
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-gate.ts +116 -7
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-status.ts +200 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-gate.ts +25 -12
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/verdict-extraction.ts +70 -11
- 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]__1mcitst._.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/{0jer_drkl1e4r.js → 0zebsmmk1bvnb.js} +4 -4
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{20x-t0a0v129z.js → 1uogpj5w2n5ju.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2ou_81ahqw31e.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/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/{5xy_GA7G5KRuE3HDumIiw → QY2GhiI47-765D9DHGy1I}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{5xy_GA7G5KRuE3HDumIiw → QY2GhiI47-765D9DHGy1I}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{5xy_GA7G5KRuE3HDumIiw → QY2GhiI47-765D9DHGy1I}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctrine-no-procedures sweep (task 10, Issue #385) — task 9's rule that
|
|
3
|
+
* doctrine prose describes no command sequence, made a check. `roles/*.md`
|
|
4
|
+
* and `contracts/*.md` explain *what* a command sequence does and *why*;
|
|
5
|
+
* they are not a runbook a reader executes verbatim, and a copy-pasted
|
|
6
|
+
* sequence rots the moment the real command changes (found live: two of
|
|
7
|
+
* this tranche's own PRs were blocked writing exactly this shape after the
|
|
8
|
+
* rule was already known in prose).
|
|
9
|
+
*
|
|
10
|
+
* Zero I/O: every input (file paths + contents) is read by the adapter
|
|
11
|
+
* (`check-doctrine-no-procedures.ts`) and passed in — same discipline as
|
|
12
|
+
* `doctrine-portability.ts` beside it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { COMMAND_WORDS, extractFencedBlocks } from './brief-validation'
|
|
16
|
+
import { VENDOR_EXAMPLE_END, VENDOR_EXAMPLE_START } from './doctrine-portability'
|
|
17
|
+
|
|
18
|
+
export type DoctrineFile = { path: string; content: string }
|
|
19
|
+
export type DoctrineProcedureFinding = { file: string; line: number; message: string }
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The one sanctioned fenced home for a real command sequence in doctrine
|
|
23
|
+
* (`tranche-model.md` §12's `AEG:VENDOR-EXAMPLE` pair) — same markers
|
|
24
|
+
* `doctrine-portability.ts` exempts from its own vendor-name scan, exported
|
|
25
|
+
* from there rather than re-declared here. First pair wins, same rule as
|
|
26
|
+
* every other `AEG:*` anchor consumer.
|
|
27
|
+
*/
|
|
28
|
+
function vendorExampleSpan(content: string): { start: number; end: number } | null {
|
|
29
|
+
const start = VENDOR_EXAMPLE_START.exec(content)
|
|
30
|
+
if (!start) return null
|
|
31
|
+
const afterStart = start.index + start[0].length
|
|
32
|
+
const end = VENDOR_EXAMPLE_END.exec(content.slice(afterStart))
|
|
33
|
+
if (!end) return null
|
|
34
|
+
return { start: start.index, end: afterStart + end.index + end[0].length }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isCommandLine(line: string): boolean {
|
|
38
|
+
const word = line.trim().split(/\s+/)[0] ?? ''
|
|
39
|
+
return (COMMAND_WORDS as readonly string[]).includes(word)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A fence's language tag that reads as shell — the empty tag (` ``` `, most
|
|
44
|
+
* doctrine command blocks) or an explicit `sh`/`bash`/`shell`/`console`.
|
|
45
|
+
* Anything else (`ts`, `js`, `yaml`, `json`, …) is a real illustration in
|
|
46
|
+
* that language, not a command sequence — a two-line ` ```ts ` block with two
|
|
47
|
+
* `export` statements is TypeScript, not shell, even though `export` is also
|
|
48
|
+
* a shell builtin (round-2 ruling item 2, found live: this task's own
|
|
49
|
+
* `brief-validation.ts` excerpt in `state-machine.md` reads exactly this
|
|
50
|
+
* way). A block outside this set never reaches the command-line count at
|
|
51
|
+
* all — it is not a candidate, regardless of its content.
|
|
52
|
+
*/
|
|
53
|
+
const SHELL_LANGS: ReadonlySet<string> = new Set(['', 'sh', 'bash', 'shell', 'console'])
|
|
54
|
+
|
|
55
|
+
function procedureLineNumberAt(content: string, index: number): number {
|
|
56
|
+
return content.slice(0, index).split('\n').length
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `aeg-root/templates/**` (or any adopter's `<doctrineRoot>/templates/**`) — worked-example templates that show the vendor-facing PR-report shape and legitimately carry a full command block. */
|
|
60
|
+
function isTemplatePath(path: string): boolean {
|
|
61
|
+
return /(^|\/)templates\//.test(path)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A fenced block containing two or more lines that each start with a shell
|
|
66
|
+
* command word is a command sequence, not an illustration — the failure
|
|
67
|
+
* names it: "this sequence is a `vinaya` command, name it." Exempt: a block
|
|
68
|
+
* inside the `AEG:VENDOR-EXAMPLE` anchor pair, and any file under a
|
|
69
|
+
* `templates/` directory.
|
|
70
|
+
*/
|
|
71
|
+
export function checkDoctrineNoProcedures(files: DoctrineFile[]): DoctrineProcedureFinding[] {
|
|
72
|
+
const findings: DoctrineProcedureFinding[] = []
|
|
73
|
+
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
if (isTemplatePath(file.path)) continue
|
|
76
|
+
const span = vendorExampleSpan(file.content)
|
|
77
|
+
|
|
78
|
+
for (const block of extractFencedBlocks(file.content)) {
|
|
79
|
+
if (span && block.start >= span.start && block.end <= span.end) continue
|
|
80
|
+
if (!SHELL_LANGS.has(block.lang)) continue
|
|
81
|
+
|
|
82
|
+
const commandLineCount = block.content.split('\n').filter(isCommandLine).length
|
|
83
|
+
if (commandLineCount >= 2) {
|
|
84
|
+
findings.push({
|
|
85
|
+
file: file.path,
|
|
86
|
+
line: procedureLineNumberAt(file.content, block.start),
|
|
87
|
+
message: `this fenced block has ${commandLineCount} shell-command lines — this sequence is a \`vinaya\` command, name it (or move it inside the \`AEG:VENDOR-EXAMPLE\` anchor / a \`templates/\` file).`
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return findings
|
|
94
|
+
}
|
|
@@ -17,10 +17,31 @@
|
|
|
17
17
|
* from), where a deny-list of "known author-repo prefixes" would fail open
|
|
18
18
|
* on the next unlisted one instead.
|
|
19
19
|
*
|
|
20
|
+
* **A second, additive dimension (Issue #298): a portable path is not the
|
|
21
|
+
* only way doctrine couples itself to one vendor.** Prose can name a
|
|
22
|
+
* specific AI company, product, or agent directly — "Claude Code", "GPT",
|
|
23
|
+
* "Anthropic" — with no path shape for the path-based predicate above to
|
|
24
|
+
* even look at. `checkDoctrinePortability` now emits a SECOND finding kind
|
|
25
|
+
* for exactly that: a fixed vendor-name word list, scanned against prose
|
|
26
|
+
* (never against code — a masked, `maskCode`-blind scan, the identical
|
|
27
|
+
* code-recognition grammar `anchored-region.ts` uses for the `AEG:*` PR/Issue
|
|
28
|
+
* body anchors, imported directly rather than re-implemented), with exactly
|
|
29
|
+
* ONE standing exemption: text inside a `<!-- AEG:VENDOR-EXAMPLE:START -->` /
|
|
30
|
+
* `<!-- AEG:VENDOR-EXAMPLE:END -->` pair — doctrine's one sanctioned, fenced
|
|
31
|
+
* home for naming today's shipped reference host by product name
|
|
32
|
+
* (`tranche-model.md` §12). A vendor word that is itself part of an
|
|
33
|
+
* already-portable path citation (`` `.claude/hooks/x.sh` ``, `` `CLAUDE.md` ``)
|
|
34
|
+
* is not re-flagged here: it is inline code, already masked by `maskCode`
|
|
35
|
+
* before the word scan ever runs, and the path-shape predicate above already
|
|
36
|
+
* has an opinion about it. This dimension has no I/O of its own either — same
|
|
37
|
+
* inputs, same adapter.
|
|
38
|
+
*
|
|
20
39
|
* Zero I/O: every input (file paths + contents) is read by the adapter and
|
|
21
40
|
* passed in.
|
|
22
41
|
*/
|
|
23
42
|
|
|
43
|
+
import { maskCode } from '@attalabs/aeg-forge-state/strip-code'
|
|
44
|
+
|
|
24
45
|
export type PortabilitySourceFile = { path: string; content: string }
|
|
25
46
|
|
|
26
47
|
export type PortabilityFinding = {
|
|
@@ -28,6 +49,8 @@ export type PortabilityFinding = {
|
|
|
28
49
|
line: number
|
|
29
50
|
cited: string
|
|
30
51
|
message: string
|
|
52
|
+
/** `'path'` — the original non-portable-path predicate. `'vendor-name'` — Issue #298's word-list predicate. */
|
|
53
|
+
kind: 'path' | 'vendor-name'
|
|
31
54
|
}
|
|
32
55
|
|
|
33
56
|
/** `aeg-root/**` by default — what this repo's own package ships and every adopter installs read-only. */
|
|
@@ -105,6 +128,93 @@ const CITED_PATH_PATTERN = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]*)+$/
|
|
|
105
128
|
*/
|
|
106
129
|
export const NON_PATH_TOP_SEGMENTS: ReadonlySet<string> = new Set(['origin', 'refs', 'HEAD', 'vinaya', 'fix'])
|
|
107
130
|
|
|
131
|
+
/**
|
|
132
|
+
* The vendor-name word list (Issue #298's re-count, live at authoring):
|
|
133
|
+
* every AI company/product/agent name this doctrine's own prose has
|
|
134
|
+
* actually used, as either the shipped reference host or a peer example —
|
|
135
|
+
* a real, hardcoded list for this repo's own corpus, not a growable
|
|
136
|
+
* config surface (brief `fix/doctrine-vendor-neutrality` §10: ship a
|
|
137
|
+
* reasonable hardcoded list, flag generalization for later). Longest
|
|
138
|
+
* alternative first ("claude code" before "claude") so a two-word mention
|
|
139
|
+
* is reported whole rather than as a truncated single-word match followed
|
|
140
|
+
* by a dangling "code".
|
|
141
|
+
*
|
|
142
|
+
* Includes model-TIER names (`opus`/`sonnet`/`haiku`), not just
|
|
143
|
+
* company/product names — a review round on this task's own PR (#338)
|
|
144
|
+
* found the first cut missed exactly this class: `brief-authoring/SKILL.md`
|
|
145
|
+
* and `brief-template.md` named a specific model tier bare, in prose, in
|
|
146
|
+
* three places the path-shape predicate could never see either. `sonnet`
|
|
147
|
+
* and `haiku` are ordinary English words outside this domain — a real
|
|
148
|
+
* false-positive risk for a general-purpose tool, accepted here on the
|
|
149
|
+
* same "reasonable hardcoded list for this repo's own corpus" basis as the
|
|
150
|
+
* rest of this list (verified against the live corpus: no non-vendor use
|
|
151
|
+
* of either word exists in `aeg-root/**` today).
|
|
152
|
+
*/
|
|
153
|
+
const VENDOR_NAME_SOURCE =
|
|
154
|
+
'\\bclaude code\\b|\\bclaude\\b|\\banthropic\\b|\\bchatgpt\\b|\\bopenai\\b|\\bgpt\\b|\\bgemini\\b|\\bcodex\\b|\\bgrok\\b|\\bdeepseek\\b|\\bopus\\b|\\bsonnet\\b|\\bhaiku\\b'
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Exported (alongside `VENDOR_EXAMPLE_END` below) so `doctrine-no-procedures.ts`
|
|
158
|
+
* can find the same fenced home's raw-text span — never a second copy of this
|
|
159
|
+
* pattern — to exempt a fenced block that sits inside it, the same way this
|
|
160
|
+
* file exempts the region from its own vendor-name word scan.
|
|
161
|
+
*/
|
|
162
|
+
export const VENDOR_EXAMPLE_START = /<!--\s*AEG:VENDOR-EXAMPLE:START\s*-->/
|
|
163
|
+
export const VENDOR_EXAMPLE_END = /<!--\s*AEG:VENDOR-EXAMPLE:END\s*-->/
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Blanks (same-length, index-preserving — same discipline as `maskCode`
|
|
167
|
+
* itself) the region between the first well-formed
|
|
168
|
+
* `<!-- AEG:VENDOR-EXAMPLE:START -->` … `<!-- AEG:VENDOR-EXAMPLE:END -->`
|
|
169
|
+
* pair in `masked` — doctrine's one sanctioned home for naming today's
|
|
170
|
+
* shipped reference host by product name (`tranche-model.md` §12).
|
|
171
|
+
* Markers are searched on already-`maskCode`d text, the same order
|
|
172
|
+
* `anchoredRegionBounds` searches the `AEG:*` PR/Issue-body anchors, so a
|
|
173
|
+
* decoy pair quoted inside a fenced example never wins. A START with no
|
|
174
|
+
* following END is not a fence at all — the same "malformed half-pair is no
|
|
175
|
+
* anchor" rule `anchored-region.ts` applies.
|
|
176
|
+
*
|
|
177
|
+
* **First pair wins, same as `anchoredRegionBounds` — a SECOND pair in the
|
|
178
|
+
* same file is not masked.** By design there is exactly one fenced home in
|
|
179
|
+
* the whole doctrine tree (the Goal this check exists to hold), so a
|
|
180
|
+
* second pair anywhere is itself a doctrine defect, not a shape this
|
|
181
|
+
* function needs to accommodate; scanning per-file rather than per-pair
|
|
182
|
+
* keeps that failure visible (a second, unmasked pair still reports its
|
|
183
|
+
* own `vendor-name` findings) instead of silently exempting it too.
|
|
184
|
+
*/
|
|
185
|
+
function maskVendorExampleRegion(masked: string): string {
|
|
186
|
+
const start = VENDOR_EXAMPLE_START.exec(masked)
|
|
187
|
+
if (!start) return masked
|
|
188
|
+
const afterStart = start.index + start[0].length
|
|
189
|
+
const end = VENDOR_EXAMPLE_END.exec(masked.slice(afterStart))
|
|
190
|
+
if (!end) return masked
|
|
191
|
+
const regionEnd = afterStart + end.index + end[0].length
|
|
192
|
+
const region = masked.slice(start.index, regionEnd)
|
|
193
|
+
const blanked = region.replace(/[^\n]/g, ' ')
|
|
194
|
+
return masked.slice(0, start.index) + blanked + masked.slice(regionEnd)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Every vendor-name-list match in `content`, code-blind (`maskCode`, the
|
|
199
|
+
* same grammar `anchored-region.ts` uses) and blind to the one fenced
|
|
200
|
+
* `AEG:VENDOR-EXAMPLE` home. A vendor word that is only part of an
|
|
201
|
+
* already-portable inline-code path citation (`` `.claude/hooks/x.sh` ``,
|
|
202
|
+
* `` `CLAUDE.md` ``) never reaches this scan at all — it is masked before
|
|
203
|
+
* the word list ever runs, the same way a fenced worked example is blind to
|
|
204
|
+
* `body-bare-digits`.
|
|
205
|
+
*/
|
|
206
|
+
function extractVendorMentions(content: string): { name: string; index: number }[] {
|
|
207
|
+
const scoped = maskVendorExampleRegion(maskCode(content))
|
|
208
|
+
const found: { name: string; index: number }[] = []
|
|
209
|
+
const pattern = new RegExp(VENDOR_NAME_SOURCE, 'gi')
|
|
210
|
+
let match: RegExpExecArray | null = pattern.exec(scoped)
|
|
211
|
+
while (match !== null) {
|
|
212
|
+
found.push({ name: match[0], index: match.index })
|
|
213
|
+
match = pattern.exec(scoped)
|
|
214
|
+
}
|
|
215
|
+
return found
|
|
216
|
+
}
|
|
217
|
+
|
|
108
218
|
/** Every inline-backtick span in `content`, tested against `CITED_PATH_PATTERN`. */
|
|
109
219
|
function extractCitedPaths(content: string): { cited: string; index: number }[] {
|
|
110
220
|
const found: { cited: string; index: number }[] = []
|
|
@@ -140,10 +250,12 @@ function isPortable(cited: string, shipsPrefix: string): boolean {
|
|
|
140
250
|
}
|
|
141
251
|
|
|
142
252
|
/**
|
|
143
|
-
* Sweeps every file under `shipsPrefix` for
|
|
144
|
-
* is not allow-listed portable
|
|
145
|
-
*
|
|
146
|
-
*
|
|
253
|
+
* Sweeps every file under `shipsPrefix` for two independent finding kinds:
|
|
254
|
+
* a cited path whose top segment is not allow-listed portable (`'path'`),
|
|
255
|
+
* and a vendor-name-list word used in prose outside the one fenced
|
|
256
|
+
* `AEG:VENDOR-EXAMPLE` home (`'vendor-name'`, Issue #298). Files outside
|
|
257
|
+
* `shipsPrefix` are out of scope entirely — this check only judges what the
|
|
258
|
+
* shipped doctrine tree itself cites/names, never a repo's other source.
|
|
147
259
|
*/
|
|
148
260
|
export function checkDoctrinePortability(
|
|
149
261
|
files: readonly PortabilitySourceFile[],
|
|
@@ -161,7 +273,18 @@ export function checkDoctrinePortability(
|
|
|
161
273
|
file: file.path,
|
|
162
274
|
line: lineAtIndex(file.content, index),
|
|
163
275
|
cited,
|
|
164
|
-
message: `cites "${cited}", a path that only exists in the authoring repository — not portable doctrine
|
|
276
|
+
message: `cites "${cited}", a path that only exists in the authoring repository — not portable doctrine`,
|
|
277
|
+
kind: 'path'
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for (const { name, index } of extractVendorMentions(file.content)) {
|
|
282
|
+
findings.push({
|
|
283
|
+
file: file.path,
|
|
284
|
+
line: lineAtIndex(file.content, index),
|
|
285
|
+
cited: name,
|
|
286
|
+
message: `names "${name}" outside the one fenced vendor-example home (an <!-- AEG:VENDOR-EXAMPLE:START --> … <!-- AEG:VENDOR-EXAMPLE:END --> pair) — portable doctrine refers to a host generically everywhere else`,
|
|
287
|
+
kind: 'vendor-name'
|
|
165
288
|
})
|
|
166
289
|
}
|
|
167
290
|
}
|
|
@@ -121,6 +121,7 @@ export const SHIPPED_BIN_AUDIENCE: Record<string, GateAudience> = {}
|
|
|
121
121
|
*/
|
|
122
122
|
export const CLI_CHECK_RING: Readonly<Record<string, 0 | 1 | 2>> = {
|
|
123
123
|
'brief-shape': 0,
|
|
124
|
+
'pr-report-density': 0,
|
|
124
125
|
'doc-coverage': 0,
|
|
125
126
|
coherence: 0,
|
|
126
127
|
'dispatch-readiness': 0,
|
|
@@ -139,8 +140,10 @@ export const CLI_CHECK_RING: Readonly<Record<string, 0 | 1 | 2>> = {
|
|
|
139
140
|
'evidence-fresh': 1,
|
|
140
141
|
'reader-resolvable-prose': 0,
|
|
141
142
|
'retired-vocabulary': 0,
|
|
143
|
+
'doctrine-no-procedures': 0,
|
|
142
144
|
'doctrine-portability': 0,
|
|
143
145
|
'workspace-escape': 0,
|
|
146
|
+
'exec-bits': 0,
|
|
144
147
|
'changeset-coverage': 0,
|
|
145
148
|
'quoted-command': 0,
|
|
146
149
|
'main-branch-refusal': 0,
|
|
@@ -10,14 +10,19 @@ export {
|
|
|
10
10
|
CROSS_CUTTING_CANDIDATES,
|
|
11
11
|
deriveBuiltinCrossCuttingDefaults,
|
|
12
12
|
deriveWorkspacePackageDomains,
|
|
13
|
-
parsePnpmWorkspaceYaml
|
|
13
|
+
parsePnpmWorkspaceYaml,
|
|
14
|
+
resolveWorkspaceEntry
|
|
14
15
|
} from './blast-radius-domains'
|
|
16
|
+
export { buildConsumersOf, deriveWorkspaceMemberDirs } from './consumer-enumeration'
|
|
17
|
+
export type { PackageManifest } from './consumer-enumeration'
|
|
15
18
|
export { parseTranche } from './parse-tranche'
|
|
16
19
|
export { deriveTranche } from './derive-tranche'
|
|
17
20
|
export { parseLedger, rowFromCells } from './parse-ledger'
|
|
18
21
|
export { sumLedger } from './sum-ledger'
|
|
19
22
|
export { aggregateTaskTokenRows, parseTokenReportEntries, parseTokensLines } from './parse-token-report'
|
|
20
23
|
export type { TokenSourcePr } from './parse-token-report'
|
|
24
|
+
export { deriveReviewStatus, parseDeveloperRoundMarker, renderReviewStatus } from './review-status'
|
|
25
|
+
export type { ReviewStatus, ReviewStatusInput } from './review-status'
|
|
21
26
|
export { formatBreakdown, formatTokenReportRow, formatTokensLine } from './report-tokens'
|
|
22
27
|
export type { TokenReportRowInput, TokensLineInput, TranscriptSummary, UsageComponents } from './report-tokens'
|
|
23
28
|
// The Claude Code collection adapter (`tranche-model.md` §12 layer 2) — one
|
|
@@ -72,9 +77,14 @@ export {
|
|
|
72
77
|
checkAutonomyClause,
|
|
73
78
|
checkBriefSections,
|
|
74
79
|
checkClosesN as checkBriefClosesN,
|
|
80
|
+
checkCommandsCarryOutput,
|
|
81
|
+
checkConsumerTests,
|
|
82
|
+
checkDefeatCases,
|
|
75
83
|
checkDocUpdateList,
|
|
76
84
|
checkForField,
|
|
77
85
|
checkForgeTitle,
|
|
86
|
+
checkNoAgentBoxes,
|
|
87
|
+
checkNoUnpinnedCodeClaims,
|
|
78
88
|
checkPlanPrNoCloses,
|
|
79
89
|
checkPremiseCoverage,
|
|
80
90
|
checkPrincipalPlaceholder,
|
|
@@ -85,14 +95,24 @@ export {
|
|
|
85
95
|
checkTestPlanExclusivity,
|
|
86
96
|
checkTierField,
|
|
87
97
|
checkWorktreeStep0,
|
|
98
|
+
AGENT_BOXES_REFUSED_SINCE_PR,
|
|
99
|
+
BRIEF_RULES_SINCE_PR,
|
|
100
|
+
COMMAND_WORDS,
|
|
88
101
|
COMMIT_TYPE_STYLE,
|
|
89
102
|
COMMIT_TYPES,
|
|
103
|
+
extractFencedBlocks,
|
|
90
104
|
headerRegion,
|
|
91
105
|
inferBranchFromBody,
|
|
92
106
|
isBriefShaped,
|
|
93
|
-
|
|
107
|
+
isGrandfatherableBriefRuleError,
|
|
108
|
+
isTaskBranch,
|
|
109
|
+
partitionBriefErrorsByRollout
|
|
94
110
|
} from './brief-validation'
|
|
95
|
-
export type { BriefSectionResult, BriefSectionsOptions } from './brief-validation'
|
|
111
|
+
export type { BriefSectionResult, BriefSectionsOptions, FencedBlock } from './brief-validation'
|
|
112
|
+
export { checkDoctrineNoProcedures } from './doctrine-no-procedures'
|
|
113
|
+
export type { DoctrineFile, DoctrineProcedureFinding } from './doctrine-no-procedures'
|
|
114
|
+
export { checkPrReportDensity, checkScopeDensity, checkSummaryDensity } from './pr-report-density'
|
|
115
|
+
export type { DensityResult } from './pr-report-density'
|
|
96
116
|
export {
|
|
97
117
|
checkA1,
|
|
98
118
|
checkA2,
|
|
@@ -299,3 +319,5 @@ export type {
|
|
|
299
319
|
} from './milestone-validation'
|
|
300
320
|
export { findWorkspaceEscapes } from './workspace-escape'
|
|
301
321
|
export type { WorkspaceEscapeFinding, WorkspaceEscapeReason, WorkspaceEscapeSourceFile } from './workspace-escape'
|
|
322
|
+
export { parseRationaleFields, renderBrief } from './brief-render'
|
|
323
|
+
export type { BriefFacts, RationaleFieldKey, RenderResult, SurfaceFileFact } from './brief-render'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { rowFromCells } from './parse-ledger'
|
|
2
2
|
import { splitTableRow } from './parse-registry'
|
|
3
3
|
import type { LedgerRow } from './types'
|
|
4
|
+
import { isPrincipal } from './waiver-label'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Live-source token parsing (aeg-forge-state-v1 task 4b, #445). Where
|
|
@@ -138,28 +139,39 @@ function isDashOnly(s: string): boolean {
|
|
|
138
139
|
return s === '—' || s === '-' || s === '–'
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* One merged PR's body + top-level comments — the caller's fetch result, no
|
|
144
|
+
* I/O here. Each comment carries its author's login (`null` when the forge
|
|
145
|
+
* reports none, e.g. a deleted account) because a comment's `Tokens:` line is
|
|
146
|
+
* only a ledger row when an allowlisted principal posted it — every agent in
|
|
147
|
+
* this model posts under the Principal's own `gh` identity, so an unfiltered
|
|
148
|
+
* read counts a stranger's pasted table as a real turn.
|
|
149
|
+
*/
|
|
142
150
|
export type TokenSourcePr = {
|
|
143
151
|
number: number
|
|
144
152
|
body: string
|
|
145
|
-
comments: string[]
|
|
153
|
+
comments: { body: string; author: string | null }[]
|
|
146
154
|
}
|
|
147
155
|
|
|
148
156
|
/**
|
|
149
157
|
* Pure aggregation over every merged PR associated with a task (its own
|
|
150
158
|
* branch's PR(s), plus any plan/cross-referenced PR carrying the Planner's
|
|
151
159
|
* report) — Developer "Token report" entries and `Tokens: …` lines from
|
|
152
|
-
* each PR body, and `Tokens: …` lines from each PR's comments
|
|
160
|
+
* each PR body, and `Tokens: …` lines from each PR's comments — the latter
|
|
161
|
+
* only from a comment an allowlisted principal authored. Missing or
|
|
153
162
|
* malformed reports simply produce no row for that report — never a
|
|
154
163
|
* fabricated figure — matching `archivist.md`'s own "flag it under DANGLING
|
|
155
164
|
* instead" discipline; the caller decides how to surface an empty result.
|
|
156
165
|
*/
|
|
157
|
-
export function aggregateTaskTokenRows(prs: TokenSourcePr[]): LedgerRow[] {
|
|
166
|
+
export function aggregateTaskTokenRows(prs: TokenSourcePr[], principalAllowlist: string[]): LedgerRow[] {
|
|
158
167
|
const out: LedgerRow[] = []
|
|
159
168
|
for (const pr of prs) {
|
|
160
169
|
out.push(...parseTokenReportEntries(pr.body))
|
|
161
170
|
out.push(...parseTokensLines(pr.body))
|
|
162
|
-
for (const comment of pr.comments)
|
|
171
|
+
for (const comment of pr.comments) {
|
|
172
|
+
if (!isPrincipal(comment.author, principalAllowlist)) continue
|
|
173
|
+
out.push(...parseTokensLines(comment.body))
|
|
174
|
+
}
|
|
163
175
|
}
|
|
164
176
|
return out
|
|
165
177
|
}
|
|
@@ -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
|
+
}
|
|
@@ -80,6 +80,41 @@ export function parsePremiseBlock(prBody: string): PremiseAssertion[] {
|
|
|
80
80
|
return assertions
|
|
81
81
|
}
|
|
82
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
|
+
|
|
83
118
|
export type PremiseCheckResult = { pass: boolean; failures: string[] }
|
|
84
119
|
|
|
85
120
|
/**
|
|
@@ -75,8 +75,8 @@ import { classifyProseFile, type ProseFileClass } from './reader-resolvable-pros
|
|
|
75
75
|
// package's own barrel comment ("reachable through the `./strip-code`
|
|
76
76
|
// subpath for the two callers that need them"). Not through
|
|
77
77
|
// `anchored-region.ts`'s re-export: that file's own `anchoredRegionBounds`
|
|
78
|
-
// is scoped to its closed
|
|
79
|
-
// marker (a free-form cited-file path, not one of those
|
|
78
|
+
// is scoped to its closed seven-field `AnchorField` grammar, which this
|
|
79
|
+
// marker (a free-form cited-file path, not one of those seven names) cannot
|
|
80
80
|
// use without widening a PR-body-specific type for a doc-authoring concern
|
|
81
81
|
// outside this task's surface. Reusing `maskCode` itself — rather than
|
|
82
82
|
// writing a second masker — is the actual "do not write a second anchor
|
|
@@ -164,7 +164,7 @@ function extractQuotedText(raw: string): string {
|
|
|
164
164
|
* (index-preserving, same primitive `anchored-region.ts` uses) to find
|
|
165
165
|
* marker positions code-blind, then slices the real inner text from the
|
|
166
166
|
* original `content` at those same indices, exactly as `anchoredRegionBounds`
|
|
167
|
-
* does for its own
|
|
167
|
+
* does for its own seven fields.
|
|
168
168
|
*/
|
|
169
169
|
function findQuotesInFile(path: string, content: string): CitedQuote[] {
|
|
170
170
|
const masked = maskCode(content)
|