@attalabs/vinaya 0.23.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 +1 -0
- 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 +7 -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 +7 -1
- 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 +7 -1
- package/dist/index.js +362 -87
- package/package.json +1 -1
- 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
package/package.json
CHANGED
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
* **Tier:** 1
|
|
40
40
|
* <!-- AEG:TIER:END -->
|
|
41
41
|
*
|
|
42
|
-
* Deliberately minimal:
|
|
42
|
+
* Deliberately minimal: seven field names, one shape. This is a delimiting
|
|
43
43
|
* convention, not a metadata DSL; resist adding structure to it.
|
|
44
44
|
*
|
|
45
|
-
* `EVIDENCE` (fix/pr-report-emitter) differs from the other
|
|
45
|
+
* `EVIDENCE` (fix/pr-report-emitter) differs from the other six in one way:
|
|
46
46
|
* it is never hand-typed. `vinaya pr report --write` is the only writer, and
|
|
47
47
|
* `check-evidence-fresh` is the only reader — no prose-fallback recognition
|
|
48
48
|
* exists or is planned for this field, unlike the anchor-optional grammar
|
|
@@ -51,8 +51,13 @@
|
|
|
51
51
|
|
|
52
52
|
import { maskCode } from '@attalabs/aeg-forge-state/strip-code'
|
|
53
53
|
|
|
54
|
-
/**
|
|
55
|
-
|
|
54
|
+
/**
|
|
55
|
+
* The seven gate-read fields with an anchored home. `TOKENS` (task 5, #378)
|
|
56
|
+
* joined here rather than through a second, parallel field list —
|
|
57
|
+
* `pr-report.ts`'s own `AEG:TOKENS:START`/`:END` markers already use this
|
|
58
|
+
* exact grammar, so the registry is the fix, not a workaround beside it.
|
|
59
|
+
*/
|
|
60
|
+
export const ANCHOR_FIELDS = ['CLOSES', 'PROJECT', 'TIER', 'PREMISE', 'TEST-PLAN', 'EVIDENCE', 'TOKENS'] as const
|
|
56
61
|
|
|
57
62
|
export type AnchorField = (typeof ANCHOR_FIELDS)[number]
|
|
58
63
|
|
|
@@ -46,8 +46,14 @@
|
|
|
46
46
|
* real-world workspaces shape and is dropped rather than guessed at — this
|
|
47
47
|
* applies identically whether the entry came in positive or negated (the
|
|
48
48
|
* caller strips the leading `!` before calling this).
|
|
49
|
+
*
|
|
50
|
+
* Exported so `consumer-enumeration.ts` can resolve every workspace glob
|
|
51
|
+
* (`apps/*` and `packages/*` alike) rather than re-deriving this same glob
|
|
52
|
+
* grammar a second time — `deriveWorkspacePackageDomains` below deliberately
|
|
53
|
+
* narrows to `packages/*` only, which is wrong for that module's purpose
|
|
54
|
+
* (task 10 round-2 ruling item 3).
|
|
49
55
|
*/
|
|
50
|
-
function resolveWorkspaceEntry(entry: string, listDirs: (dir: string) => string[]): string[] {
|
|
56
|
+
export function resolveWorkspaceEntry(entry: string, listDirs: (dir: string) => string[]): string[] {
|
|
51
57
|
if (entry.endsWith('/*')) {
|
|
52
58
|
const baseDir = entry.slice(0, -2)
|
|
53
59
|
return listDirs(baseDir).map((name) => `${baseDir}/${name}`)
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task-brief renderer (review-convergence-v1 task 12, #387). Pure — no `fs`,
|
|
3
|
+
* no `gh`/`git`, no `process.env`. Reads `aeg-root/templates/brief-template.md`
|
|
4
|
+
* (passed in as `template`, read at run time by the CLI shim
|
|
5
|
+
* `apps/cli/src/commands/brief.ts`) and every derivable fact (`BriefFacts`,
|
|
6
|
+
* assembled by that same shim from the forge and the tree) and emits the
|
|
7
|
+
* twelve-section brief skeleton with every mechanically-derivable section
|
|
8
|
+
* filled. A section this module cannot derive from a stated fact is never
|
|
9
|
+
* defaulted — `renderBrief` refuses, naming the missing fact, per Issue #387's
|
|
10
|
+
* rule: "no section is written by hand; the rationale is the one hand-written
|
|
11
|
+
* artefact and the Issue creation gate is its review."
|
|
12
|
+
*
|
|
13
|
+
* Every judgment section (Technical dependencies' free-form detail beyond the
|
|
14
|
+
* Dependency-rationale field, the Pre-flight task-specific checks, the
|
|
15
|
+
* Verification command list beyond the fixed full run) is out of this task's
|
|
16
|
+
* scope — Issue #387: "NOT this task: rendering any judgment section." Those
|
|
17
|
+
* sections are rendered with the mechanical content this module CAN derive;
|
|
18
|
+
* anything genuinely judgment-only is left for the Brief Author to add by
|
|
19
|
+
* hand after render.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { deriveSection7 } from './derive-section7'
|
|
23
|
+
import { isDocFile } from './file-classify'
|
|
24
|
+
import { deriveTierFromDiff } from './pr-tier'
|
|
25
|
+
|
|
26
|
+
export type RationaleFieldKey =
|
|
27
|
+
| 'boundary'
|
|
28
|
+
| 'dependencyRationale'
|
|
29
|
+
| 'trapsToAvoid'
|
|
30
|
+
| 'suggestedAgentClass'
|
|
31
|
+
| 'stopAndEscalate'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Tolerant label patterns for the five rationale fields this renderer
|
|
35
|
+
* consumes — the same patterns `issue-validation.ts`'s `checkIssueRationale`
|
|
36
|
+
* uses for presence, so a field this renderer can read is exactly a field
|
|
37
|
+
* that gate already required present. Not imported directly:
|
|
38
|
+
* `issue-validation.ts` keeps its own copy private (`RATIONALE_FIELDS`), and
|
|
39
|
+
* duplicating the five patterns here is cheaper than exporting a new surface
|
|
40
|
+
* from a file outside this task's boundary.
|
|
41
|
+
*/
|
|
42
|
+
const RATIONALE_FIELD_PATTERNS: Record<RationaleFieldKey, string> = {
|
|
43
|
+
boundary: 'Boundary',
|
|
44
|
+
dependencyRationale: 'Dependency rationale|Depends[- ]on',
|
|
45
|
+
trapsToAvoid: 'Traps',
|
|
46
|
+
suggestedAgentClass: '(?:Suggested\\s+)?agent-class',
|
|
47
|
+
stopAndEscalate: 'Stop-and-escalate'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const RATIONALE_FIELD_NAMES: Record<RationaleFieldKey, string> = {
|
|
51
|
+
boundary: 'Boundary',
|
|
52
|
+
dependencyRationale: 'Dependency rationale',
|
|
53
|
+
trapsToAvoid: 'Traps to avoid',
|
|
54
|
+
suggestedAgentClass: 'Suggested agent-class',
|
|
55
|
+
stopAndEscalate: 'Stop-and-escalate'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Slices one rationale field's prose out of an Issue body: from its label to
|
|
60
|
+
* the next bold/heading field, or the end. Adapted from `issue-validation.ts`'s
|
|
61
|
+
* private `rationaleFieldText` (see that function's doc comment for why the
|
|
62
|
+
* terminator is `(?![\s\S])`, not `$`, and why the label pattern must stay
|
|
63
|
+
* grouped) — duplicated rather than imported because that helper is not
|
|
64
|
+
* exported and `issue-validation.ts` is out of this task's surface.
|
|
65
|
+
*
|
|
66
|
+
* Anchors the label's own `**`/heading marker to the START of a line
|
|
67
|
+
* (`^\s*`), unlike the copied original — a presence-only check can tolerate
|
|
68
|
+
* matching a field name mentioned in passing mid-paragraph (the field is
|
|
69
|
+
* still present somewhere), but an EXTRACTION cannot: found live, self-
|
|
70
|
+
* rendering this very task's own Issue (#387), whose "Boundary" field
|
|
71
|
+
* prose contains the inline aside "`For:`/`Reason:` from
|
|
72
|
+
* **Suggested agent-class**" — the unanchored original matched that inline
|
|
73
|
+
* mention first (it is merely preceded by literal `**` characters
|
|
74
|
+
* somewhere in the text, never required to start a line) and returned the
|
|
75
|
+
* Boundary field's own tail as the "Suggested agent-class" field's content.
|
|
76
|
+
*/
|
|
77
|
+
function sliceRationaleField(text: string, labelPattern: string): string {
|
|
78
|
+
const re = new RegExp(
|
|
79
|
+
`^\\s*(?:\\*\\*|#{1,4}\\s+)\\s*(?:${labelPattern})[^\\n]*\\n?([\\s\\S]*?)(?=\\n\\s*(?:\\*\\*[A-Z]|#{1,4}\\s)|(?![\\s\\S]))`,
|
|
80
|
+
'im'
|
|
81
|
+
)
|
|
82
|
+
const m = re.exec(text)
|
|
83
|
+
return m ? m[0].trim() : ''
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Reads the five rationale fields this renderer needs out of a task Issue's
|
|
88
|
+
* body, tolerant of both live serializations `checkIssueRationale` accepts
|
|
89
|
+
* (`**Field** — …` bold-inline, `### Field` heading). A field absent from the
|
|
90
|
+
* body is simply absent from the returned record — never an empty string —
|
|
91
|
+
* so `renderBrief`'s missing-fact check can tell "absent" from "present but
|
|
92
|
+
* empty".
|
|
93
|
+
*/
|
|
94
|
+
export function parseRationaleFields(body: string): Partial<Record<RationaleFieldKey, string>> {
|
|
95
|
+
const out: Partial<Record<RationaleFieldKey, string>> = {}
|
|
96
|
+
for (const key of Object.keys(RATIONALE_FIELD_PATTERNS) as RationaleFieldKey[]) {
|
|
97
|
+
const text = sliceRationaleField(body, RATIONALE_FIELD_PATTERNS[key])
|
|
98
|
+
if (text) out[key] = text
|
|
99
|
+
}
|
|
100
|
+
return out
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** One §4 surface-map file: its path, a whole-file `sha256` premise pin, and the workspace package that owns it (`null` for a file outside every workspace member, e.g. a doctrine file under `aeg-root/`). */
|
|
104
|
+
export type SurfaceFileFact = {
|
|
105
|
+
path: string
|
|
106
|
+
/** `null` for a file that does not exist yet on disk (a "Create" entry) — a not-yet-created file has no current sha256 to pin. */
|
|
107
|
+
sha256: string | null
|
|
108
|
+
/** The owning workspace package's own `name` field (e.g. `@attalabs/aeg-core`) — never a directory path; this is what feeds `bunx turbo test --filter=<packageName>` directly. */
|
|
109
|
+
packageName: string | null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type BriefFacts = {
|
|
113
|
+
trancheSlug: string
|
|
114
|
+
taskId: string
|
|
115
|
+
title: string
|
|
116
|
+
issue: number
|
|
117
|
+
projects: string[]
|
|
118
|
+
dependsOn: string[]
|
|
119
|
+
conflictsWith: string[]
|
|
120
|
+
rationale: Partial<Record<RationaleFieldKey, string>>
|
|
121
|
+
dispatchReady: boolean
|
|
122
|
+
dispatchBlockers: string[]
|
|
123
|
+
surfaceFiles: SurfaceFileFact[]
|
|
124
|
+
/** Workspace package directories (e.g. `apps/cli`) that depend on `@attalabs/<pkg>` — same shape `checkConsumerTests` consumes, injected rather than derived here (this module reads no `fs`). */
|
|
125
|
+
consumersOf: (pkg: string) => string[]
|
|
126
|
+
/** `.vinaya/doc-owners` file content, or `null` when the file is absent — passed through to `deriveSection7` unchanged. */
|
|
127
|
+
docOwnersContent: string | null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type RenderResult = { ok: true; brief: string } | { ok: false; missing: string[] }
|
|
131
|
+
|
|
132
|
+
function bulletList(items: string[]): string {
|
|
133
|
+
return items.map((i) => `- ${i}`).join('\n')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isTestFile(path: string): boolean {
|
|
137
|
+
return /\.test\.[jt]sx?$/i.test(path)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const PRINCIPAL_OBSERVATION_RE = /\bprincipal\b[^.\n]*\b(?:browser|signed-in|visual|session)\b/i
|
|
141
|
+
|
|
142
|
+
function renderHeader(facts: BriefFacts, template: string): string {
|
|
143
|
+
const introMatch = template.match(/^You are the AEG Developer\.[^\n]*$/m)
|
|
144
|
+
const intro = introMatch
|
|
145
|
+
? (introMatch[0] as string)
|
|
146
|
+
.replace(/\s*\[[^\]]*\]/g, '')
|
|
147
|
+
.replace(/\bBoth mandatory\.$/, 'Mandatory.')
|
|
148
|
+
.trim()
|
|
149
|
+
: 'You are the AEG Developer. Read `aeg-root/roles/developer.md` first. Mandatory.'
|
|
150
|
+
|
|
151
|
+
const reason = facts.rationale.suggestedAgentClass ?? ''
|
|
152
|
+
// `deriveTierFromDiff` never returns `3` — Tier 3 stays a judgment call the
|
|
153
|
+
// Brief Author raises by hand; the mechanical floor this renderer can prove
|
|
154
|
+
// is 0 or 1, exactly the two values that check itself is capable of ruling
|
|
155
|
+
// out for a doc/spec-touching surface.
|
|
156
|
+
const tier = deriveTierFromDiff(facts.surfaceFiles.map((f) => f.path))
|
|
157
|
+
const lines = [
|
|
158
|
+
'**For:** [model] (coding-agent CLI on a dev machine, dispatched locally, unattended)',
|
|
159
|
+
`**Reason:** ${reason}`,
|
|
160
|
+
'**Owner:** the Principal',
|
|
161
|
+
`**Goal:** ${facts.title}`,
|
|
162
|
+
`**Project:** ${facts.projects.join(', ')}`,
|
|
163
|
+
`**Tier:** ${tier}`,
|
|
164
|
+
'',
|
|
165
|
+
`Closes #${facts.issue}`,
|
|
166
|
+
'',
|
|
167
|
+
intro
|
|
168
|
+
]
|
|
169
|
+
return lines.join('\n')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function renderSection2(facts: BriefFacts): string {
|
|
173
|
+
const depends = facts.dependsOn.length > 0 ? facts.dependsOn.join(', ') : '—'
|
|
174
|
+
const conflicts = facts.conflictsWith.length > 0 ? facts.conflictsWith.join(', ') : '—'
|
|
175
|
+
const lines = [
|
|
176
|
+
'## 2. Context — read before doing anything',
|
|
177
|
+
'',
|
|
178
|
+
`- **Tranche:** \`${facts.trancheSlug}\`, task ${facts.taskId}, Issue #${facts.issue}. Branch \`task/${facts.trancheSlug}/${facts.taskId}\`. \`Depends-on: ${depends}\`, \`Conflicts-with: ${conflicts}\`. Confirm \`READY TO DISPATCH\` at your own Step 0.`,
|
|
179
|
+
`- **Read Issue #${facts.issue} in full** for the complete rationale — do not re-derive it.`,
|
|
180
|
+
`- ${facts.rationale.boundary}`,
|
|
181
|
+
`- ${facts.rationale.trapsToAvoid}`
|
|
182
|
+
]
|
|
183
|
+
return lines.join('\n')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function renderSection3(facts: BriefFacts): string {
|
|
187
|
+
return ['## 3. Technical dependencies', '', `${facts.rationale.dependencyRationale}`].join('\n')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function renderSection4(facts: BriefFacts): string {
|
|
191
|
+
const created = facts.surfaceFiles.filter((f) => f.sha256 === null).map((f) => f.path)
|
|
192
|
+
const modified = facts.surfaceFiles.filter((f) => f.sha256 !== null).map((f) => f.path)
|
|
193
|
+
|
|
194
|
+
const byPackage = new Map<string, SurfaceFileFact[]>()
|
|
195
|
+
for (const f of facts.surfaceFiles) {
|
|
196
|
+
if (!f.packageName) continue
|
|
197
|
+
const list = byPackage.get(f.packageName) ?? []
|
|
198
|
+
list.push(f)
|
|
199
|
+
byPackage.set(f.packageName, list)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// checkConsumerTests (brief-validation.ts) requires, for every workspace
|
|
203
|
+
// package a touched `packages/<pkg>` consumer depends on, either a named
|
|
204
|
+
// test path under that consumer OR the `consumer-tests: none — <reason>`
|
|
205
|
+
// sentinel — ONE sentinel occurrence anywhere in §4 satisfies the whole
|
|
206
|
+
// section, so a single combined line covers every uncovered consumer.
|
|
207
|
+
const uncovered: string[] = []
|
|
208
|
+
for (const pkg of byPackage.keys()) {
|
|
209
|
+
const shortName = pkg.replace(/^@[^/]+\//, '')
|
|
210
|
+
for (const consumer of facts.consumersOf(shortName)) {
|
|
211
|
+
const covered = facts.surfaceFiles.some((f) => f.path.startsWith(`${consumer}/`) && isTestFile(f.path))
|
|
212
|
+
if (!covered) uncovered.push(`${pkg} (${consumer})`)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const consumerLines =
|
|
216
|
+
uncovered.length > 0
|
|
217
|
+
? [
|
|
218
|
+
`- consumer-tests: none — no consumer test path named yet for ${uncovered.join(', ')}; name one before dispatch, or confirm no consumer-facing behavior changed.`
|
|
219
|
+
]
|
|
220
|
+
: []
|
|
221
|
+
|
|
222
|
+
const lines = [
|
|
223
|
+
'## 4. Technical surface map',
|
|
224
|
+
'',
|
|
225
|
+
'**Create:**',
|
|
226
|
+
created.length > 0 ? bulletList(created) : '- (none — every surface file already exists)',
|
|
227
|
+
'',
|
|
228
|
+
'**Modify:**',
|
|
229
|
+
modified.length > 0 ? bulletList(modified) : '- (none named)',
|
|
230
|
+
...(consumerLines.length > 0 ? ['', ...consumerLines] : []),
|
|
231
|
+
'',
|
|
232
|
+
'**Out of surface:** [named explicitly by the Brief Author — not mechanically derivable]',
|
|
233
|
+
'',
|
|
234
|
+
'#### Premise pins',
|
|
235
|
+
'',
|
|
236
|
+
'**Premise:**',
|
|
237
|
+
bulletList(
|
|
238
|
+
facts.surfaceFiles
|
|
239
|
+
.filter((f): f is SurfaceFileFact & { sha256: string } => f.sha256 !== null)
|
|
240
|
+
.map((f) => `${f.path} sha256: ${f.sha256}`)
|
|
241
|
+
)
|
|
242
|
+
]
|
|
243
|
+
return lines.join('\n')
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function renderSection5(facts: BriefFacts): string {
|
|
247
|
+
const lines = [
|
|
248
|
+
'## 5. Pre-flight checks',
|
|
249
|
+
'',
|
|
250
|
+
'**Step 0 (mandatory, verbatim):**',
|
|
251
|
+
'',
|
|
252
|
+
'```',
|
|
253
|
+
`git worktree add .worktrees/task/${facts.trancheSlug}/${facts.taskId} -b task/${facts.trancheSlug}/${facts.taskId} origin/main && cd .worktrees/task/${facts.trancheSlug}/${facts.taskId} && bun install --frozen-lockfile --silent`,
|
|
254
|
+
'```',
|
|
255
|
+
'',
|
|
256
|
+
'1. Clean status; parent `origin/main`; branch suffix literal-matches the task id.',
|
|
257
|
+
'2. `bun packages/aeg-core/bin/verify-dispatch.ts <tranche> <n>` → `READY TO DISPATCH` (re-derived at render time: it was).',
|
|
258
|
+
'',
|
|
259
|
+
'On any failure: STOP and report.'
|
|
260
|
+
]
|
|
261
|
+
return lines.join('\n')
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function renderSection6(facts: BriefFacts): string {
|
|
265
|
+
const byPackage = new Map<string, SurfaceFileFact[]>()
|
|
266
|
+
const rootFiles: SurfaceFileFact[] = []
|
|
267
|
+
for (const f of facts.surfaceFiles) {
|
|
268
|
+
if (!f.packageName) {
|
|
269
|
+
rootFiles.push(f)
|
|
270
|
+
continue
|
|
271
|
+
}
|
|
272
|
+
const list = byPackage.get(f.packageName) ?? []
|
|
273
|
+
list.push(f)
|
|
274
|
+
byPackage.set(f.packageName, list)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const parts: string[] = []
|
|
278
|
+
let n = 1
|
|
279
|
+
for (const [pkg, files] of byPackage) {
|
|
280
|
+
parts.push(
|
|
281
|
+
[
|
|
282
|
+
`${n}. **Part ${n}:** ${facts.rationale.boundary}`,
|
|
283
|
+
'',
|
|
284
|
+
' Files:',
|
|
285
|
+
...files.map((f) => ` - ${f.path}`),
|
|
286
|
+
'',
|
|
287
|
+
` Run \`bunx turbo test --affected\` before committing this Part; touches ${pkg}.`
|
|
288
|
+
].join('\n')
|
|
289
|
+
)
|
|
290
|
+
n++
|
|
291
|
+
}
|
|
292
|
+
if (rootFiles.length > 0) {
|
|
293
|
+
parts.push(
|
|
294
|
+
[`${n}. **Part ${n}:** doctrine/root files.`, '', ' Files:', ...rootFiles.map((f) => ` - ${f.path}`)].join(
|
|
295
|
+
'\n'
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return ['## 6. Numbered parts — commit after EACH part; push once, before opening the PR', '', ...parts].join('\n')
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function renderSection7(section7Pointers: string[]): string {
|
|
304
|
+
const lines = ['## 7. Documentation-update list', '']
|
|
305
|
+
if (section7Pointers.length > 0) {
|
|
306
|
+
lines.push(bulletList(section7Pointers))
|
|
307
|
+
} else {
|
|
308
|
+
lines.push(
|
|
309
|
+
"`.vinaya/doc-owners` derivation matched zero bindings against this task's surface — no mechanically-derived doc updates. Confirm no additional doc artifact applies before treating this list as final."
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
return lines.join('\n')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function renderSection8(): string {
|
|
316
|
+
return [
|
|
317
|
+
'## 8. Verification before claiming done',
|
|
318
|
+
'',
|
|
319
|
+
// Never `rm -rf apps/cli/dist &&`-prefixed (Principal ruling, PR
|
|
320
|
+
// `open-1`): that removal is a Developer pre-flight step in doctrine,
|
|
321
|
+
// never a command a check or `pr report` executes — `evidence-fresh`
|
|
322
|
+
// re-running it under the twenty-six sibling checks CI had just built
|
|
323
|
+
// deleted their own `dist` out from under them.
|
|
324
|
+
'- `bunx turbo test --affected --force` (the command and its output are attested in the Evidence block).',
|
|
325
|
+
"- The full `bun run test` suite is CI's to run, on the one push — never run it locally.",
|
|
326
|
+
'- Every blast-radius consumer named in §4, re-verified by name.',
|
|
327
|
+
'- `roles/developer.md`\'s tier checklist genuinely satisfied, and `PR_BODY="$(cat <body-file>)" bun packages/aeg-core/bin/verify-docs.ts --pr` green.'
|
|
328
|
+
].join('\n')
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function renderSection9(facts: BriefFacts): string {
|
|
332
|
+
const runtimeFiles = facts.surfaceFiles.filter((f) => !isDocFile(f.path))
|
|
333
|
+
if (runtimeFiles.length === 0) {
|
|
334
|
+
// Deliberately unbolded: `locateTestPlanSection`'s heading-form slicer
|
|
335
|
+
// treats a bold `**Field:**`-shaped line as the START of the NEXT
|
|
336
|
+
// section (`NEXT_SECTION_RE`) — a bolded `**Test Plan:** unit-tests-only`
|
|
337
|
+
// line right under the `## 9. Test Plan` heading would cut the section
|
|
338
|
+
// boundary before its own content, leaving `checkTestPlan` nothing to
|
|
339
|
+
// find. Plain `Test Plan: unit-tests-only` still satisfies
|
|
340
|
+
// `TEST_PLAN_UNIT_TESTS_ONLY_RE` (both forms are equivalent to it).
|
|
341
|
+
return ['## 9. Test Plan', '', 'Test Plan: unit-tests-only'].join('\n')
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const testFiles = runtimeFiles.filter((f) => isTestFile(f.path))
|
|
345
|
+
const commandLines =
|
|
346
|
+
testFiles.length > 0
|
|
347
|
+
? testFiles.map((f) => `bun test ${f.path} → 0 fail`)
|
|
348
|
+
: ['bunx turbo test --affected --force → summary line ends "0 fail"']
|
|
349
|
+
|
|
350
|
+
const observationText = [
|
|
351
|
+
facts.rationale.boundary ?? '',
|
|
352
|
+
facts.rationale.trapsToAvoid ?? '',
|
|
353
|
+
facts.rationale.stopAndEscalate ?? ''
|
|
354
|
+
].join('\n')
|
|
355
|
+
const principalLine = PRINCIPAL_OBSERVATION_RE.test(observationText)
|
|
356
|
+
? [
|
|
357
|
+
'',
|
|
358
|
+
"- [ ] **[principal]** Verify the change described in this task's rationale in a real signed-in session/browser."
|
|
359
|
+
]
|
|
360
|
+
: []
|
|
361
|
+
|
|
362
|
+
return ['## 9. Test Plan', '', '```', ...commandLines, '```', ...principalLine].join('\n')
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function renderSection10(facts: BriefFacts): string {
|
|
366
|
+
return ['## 10. Stop conditions', '', `${facts.rationale.stopAndEscalate}`].join('\n')
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function renderSection11(template: string, facts: BriefFacts): string {
|
|
370
|
+
const trapsText = facts.rationale.trapsToAvoid ?? ''
|
|
371
|
+
const doNotLines = trapsText
|
|
372
|
+
.split('\n')
|
|
373
|
+
.map((l) => l.trim())
|
|
374
|
+
.filter((l) => /^\(?\d+\)?[.)]?\s/.test(l) || l.startsWith('-'))
|
|
375
|
+
.map((l) => l.replace(/^\(?\d+\)?[.)]?\s*/, '').replace(/^-\s*/, ''))
|
|
376
|
+
.filter((l) => l.length > 0)
|
|
377
|
+
.map((l) => `- Do NOT ${l.charAt(0).toLowerCase()}${l.slice(1)}`)
|
|
378
|
+
|
|
379
|
+
const autonomyMatch = template.match(/> \*\*Autonomy:\*\*[\s\S]*?(?=\n\n## \d+\. Deliverable|\n\n## Deliverable|$)/)
|
|
380
|
+
const autonomy = autonomyMatch ? (autonomyMatch[0] as string).trim() : ''
|
|
381
|
+
|
|
382
|
+
return ['## 11. Constraints', '', ...doNotLines, '', autonomy].join('\n')
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function renderSection12(facts: BriefFacts): string {
|
|
386
|
+
return [
|
|
387
|
+
'## 12. Deliverable',
|
|
388
|
+
'',
|
|
389
|
+
`- PR title (exact): \`[${facts.trancheSlug}] ${facts.taskId} — ${facts.title}\``,
|
|
390
|
+
'- Open the PR only via `bun apps/cli/src/index.ts pr create --body-file <path> --title "<title above>"`.',
|
|
391
|
+
"- PR body = the Developer's PR report (start from `aeg-root/templates/pr-report-template.md`), with this entire brief pasted as the reference copy inside a collapsed `<details>` block, and `Closes #" +
|
|
392
|
+
facts.issue +
|
|
393
|
+
'` at the top of the header block.',
|
|
394
|
+
'- Pre-open gate: tier checklist satisfied, and `PR_BODY="$(cat <body-file>)" bun packages/aeg-core/bin/verify-docs.ts --pr` green.',
|
|
395
|
+
'- Include `git diff main --stat` and a token report (if unavailable, state so).',
|
|
396
|
+
'- Then STOP. Review and Verification are separate invocations.'
|
|
397
|
+
].join('\n')
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Emits the twelve-section brief skeleton from `facts`, reading `template`
|
|
402
|
+
* (the raw content of `aeg-root/templates/brief-template.md`) only for the
|
|
403
|
+
* one fixed sentence (`renderHeader`'s intro line) and the standing autonomy
|
|
404
|
+
* clause (`renderSection11`) — every other byte is generated from `facts`,
|
|
405
|
+
* never copied from a hardcoded template string in this module.
|
|
406
|
+
*
|
|
407
|
+
* Refuses — `{ ok: false, missing }` — the moment a fact this renderer needs
|
|
408
|
+
* is absent: never a default, per Issue #387's rule that a brief states no
|
|
409
|
+
* fact it did not actually derive.
|
|
410
|
+
*/
|
|
411
|
+
export function renderBrief(facts: BriefFacts, template: string): RenderResult {
|
|
412
|
+
const missing: string[] = []
|
|
413
|
+
|
|
414
|
+
if (facts.projects.length === 0) missing.push('Project (task has no Project(s) declared)')
|
|
415
|
+
if (!facts.dispatchReady) missing.push(...facts.dispatchBlockers)
|
|
416
|
+
|
|
417
|
+
for (const key of Object.keys(RATIONALE_FIELD_PATTERNS) as RationaleFieldKey[]) {
|
|
418
|
+
if (!facts.rationale[key]) missing.push(RATIONALE_FIELD_NAMES[key])
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (missing.length > 0) return { ok: false, missing }
|
|
422
|
+
|
|
423
|
+
const section7Pointers = facts.docOwnersContent
|
|
424
|
+
? deriveSection7Pointers(
|
|
425
|
+
facts.surfaceFiles.map((f) => f.path),
|
|
426
|
+
facts.docOwnersContent
|
|
427
|
+
)
|
|
428
|
+
: []
|
|
429
|
+
|
|
430
|
+
const brief = [
|
|
431
|
+
renderHeader(facts, template),
|
|
432
|
+
'',
|
|
433
|
+
renderSection2(facts),
|
|
434
|
+
'',
|
|
435
|
+
renderSection3(facts),
|
|
436
|
+
'',
|
|
437
|
+
renderSection4(facts),
|
|
438
|
+
'',
|
|
439
|
+
renderSection5(facts),
|
|
440
|
+
'',
|
|
441
|
+
renderSection6(facts),
|
|
442
|
+
'',
|
|
443
|
+
renderSection7(section7Pointers),
|
|
444
|
+
'',
|
|
445
|
+
renderSection8(),
|
|
446
|
+
'',
|
|
447
|
+
renderSection9(facts),
|
|
448
|
+
'',
|
|
449
|
+
renderSection10(facts),
|
|
450
|
+
'',
|
|
451
|
+
renderSection11(template, facts),
|
|
452
|
+
'',
|
|
453
|
+
renderSection12(facts)
|
|
454
|
+
].join('\n')
|
|
455
|
+
|
|
456
|
+
return { ok: true, brief }
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** `deriveSection7`'s `pointers` alone — the only part of its result `renderSection7` needs. */
|
|
460
|
+
function deriveSection7Pointers(intendedSurfaces: string[], docOwnersContent: string): string[] {
|
|
461
|
+
return deriveSection7(intendedSurfaces, docOwnersContent).pointers
|
|
462
|
+
}
|