@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,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* diagram-model.ts — the pure derivation at the heart of one library
|
|
3
|
+
* turns doctrine (`enforcement.md` rings/gates/checks + role/contract
|
|
4
|
+
* frontmatter + the canonical `ACTIONS` set) plus declarative config plus
|
|
5
|
+
* live status into a renderer-agnostic `DiagramModel`. Every diagram consumer
|
|
6
|
+
* (Studio, the Vinaya portal, a CLI visualizer) reads THIS model; none
|
|
7
|
+
* re-implements the governance logic. Zero I/O, zero `fs`/`node:`
|
|
8
|
+
* imports — doctrine arrives already-read through `DoctrineContent`;
|
|
9
|
+
* the file reads live behind `DoctrineSource` in `@attalabs/vinaya-sources`.
|
|
10
|
+
*
|
|
11
|
+
* What is NOT here: geometry. Radius, angle, colour, coordinates are the
|
|
12
|
+
* renderer's job (a ring index and a render-state are structural facts; a
|
|
13
|
+
* pixel is not). Render-state is DERIVED, never authored: `disabled` when
|
|
14
|
+
* config disables it, else `active`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import matter from 'gray-matter'
|
|
18
|
+
import { ACTIONS, CROSSING_KEYWORDS } from './actions'
|
|
19
|
+
import type { DoctrineContent } from './doctrine-source'
|
|
20
|
+
import { findHeadingLine, findTable } from './markdown-table'
|
|
21
|
+
import { parseEnforcementRegistry } from './registry-parse'
|
|
22
|
+
import type { Tranche } from './types'
|
|
23
|
+
|
|
24
|
+
export type DiagramNodeKind = 'ring' | 'gate' | 'check' | 'action' | 'role' | 'contract'
|
|
25
|
+
export type RenderState = 'active' | 'disabled'
|
|
26
|
+
|
|
27
|
+
export type DiagramNode = {
|
|
28
|
+
id: string
|
|
29
|
+
kind: DiagramNodeKind
|
|
30
|
+
label: string
|
|
31
|
+
ringIndex?: 0 | 1 | 2
|
|
32
|
+
renderState: RenderState
|
|
33
|
+
sourceLine?: number
|
|
34
|
+
summary?: string
|
|
35
|
+
/** What this node actually does, in one plain sentence — every kind carries
|
|
36
|
+
* one, each from its own source's `description`: the enforcement table's
|
|
37
|
+
* Description column for `gate`/`check`, `description:` frontmatter for
|
|
38
|
+
* `role`/`contract`, the `ACTIONS` entry's field for `action`. One register
|
|
39
|
+
* for every kind, because a reader cannot see what kind of node they
|
|
40
|
+
* clicked. Never `GateRow.spec` — that column is written to enforce, not to
|
|
41
|
+
* read (38% of rows cite a decision id, an issue number or a file path;
|
|
42
|
+
* longest is 2650 chars), and rendering it here is the bug this field
|
|
43
|
+
* exists to prevent. `summary` asks the question, this answers it, and
|
|
44
|
+
* "Read more" carries the spec. Optional because a source may omit it,
|
|
45
|
+
* never because a kind structurally lacks one. */
|
|
46
|
+
detail?: string
|
|
47
|
+
category?: 'ci' | 'hook' | 'event'
|
|
48
|
+
actorType?: 'agent' | 'human' | 'either'
|
|
49
|
+
/** Whether this action reaches GitHub — `action` nodes only, straight from
|
|
50
|
+
* its `ACTIONS` entry (the canonical set). On the node because a
|
|
51
|
+
* renderer cannot get it any other way: `ACTIONS` is a value export, and
|
|
52
|
+
* importing it into a client component drags `@attalabs/aeg-forge-state`'s
|
|
53
|
+
* `node:child_process` into the browser bundle. The distinction is
|
|
54
|
+
* doctrine-load-bearing — ring-0 gates guard exactly the crossings, and
|
|
55
|
+
* G3 exists to prove there is no unguarded one — so it must survive to the
|
|
56
|
+
* render rather than being implied by which ring a node was filed under. */
|
|
57
|
+
crosses?: 'into-github' | 'none'
|
|
58
|
+
/** The node's name **for a reader**, when its `label` is an identifier rather
|
|
59
|
+
* than words. `role`/`contract` nodes label themselves with their doctrine id
|
|
60
|
+
* (`brief-author`, `archivist-tranche-archivist`) because that id is the
|
|
61
|
+
* node's identity — routes, doc paths and edge endpoints all key off it, so
|
|
62
|
+
* it cannot be prettified in place. This carries the doc's own `title:`
|
|
63
|
+
* frontmatter instead, and every renderer showing a name to a person prefers
|
|
64
|
+
* it, falling back to `label` when absent.
|
|
65
|
+
*
|
|
66
|
+
* Deliberately NOT `sidebar_title`: that field is the docs nav's own caption,
|
|
67
|
+
* and sourcing a doctrine node's name from a nav hint would let a caption
|
|
68
|
+
* edit silently rename the node. `title:` is the document's own name, which
|
|
69
|
+
* is what a display name should be. */
|
|
70
|
+
displayLabel?: string
|
|
71
|
+
/** Where this node sits in the process, from its doc's `order:` frontmatter
|
|
72
|
+
* — `role`/`contract` nodes only. Doctrine files are read in filename order,
|
|
73
|
+
* which is alphabetical and therefore meaningless to a reader; the process
|
|
74
|
+
* runs plan → brief → build → review → verify → merge → archive, and that is
|
|
75
|
+
* the order every surface listing these nodes should present them in. Kept on
|
|
76
|
+
* the node rather than re-derived per renderer so the docs nav and the map
|
|
77
|
+
* cannot disagree about the sequence. Absent means unordered — a renderer
|
|
78
|
+
* sorting by it must treat that as "last", never as zero. */
|
|
79
|
+
order?: number
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type DiagramEdge = {
|
|
83
|
+
id: string
|
|
84
|
+
kind: 'guards' | 'performs' | 'produces' | 'consumes'
|
|
85
|
+
from: string
|
|
86
|
+
to: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type DiagramFinding = { configKey?: string; reason: string }
|
|
90
|
+
|
|
91
|
+
export type DiagramConfig = {
|
|
92
|
+
rings?: { ring1_forgeWriteInterception?: boolean; ring2_asyncAudits?: boolean }
|
|
93
|
+
gates?: Record<string, boolean>
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type DiagramModel = {
|
|
97
|
+
nodes: DiagramNode[]
|
|
98
|
+
edges: DiagramEdge[]
|
|
99
|
+
findings: DiagramFinding[]
|
|
100
|
+
tranche: Tranche | null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Deterministic slug from a table row's first cell: lowercase, drop
|
|
104
|
+
* backticks/bold/markdown-link chrome, collapse non-alphanumerics to `-`. */
|
|
105
|
+
function slugify(cell: string): string {
|
|
106
|
+
return cell
|
|
107
|
+
.toLowerCase()
|
|
108
|
+
.replace(/`/g, '')
|
|
109
|
+
.replace(/\*\*/g, '')
|
|
110
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
111
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
112
|
+
.replace(/^-+|-+$/g, '')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The three ring summary labels, keyed by ring index, from `enforcement.md`'s
|
|
116
|
+
* `## The model:` summary table (the same table `loadRings` reads). Throws on a
|
|
117
|
+
* malformed doctrine — same contract as `parseEnforcementRegistry`.
|
|
118
|
+
*
|
|
119
|
+
* Keyed by row POSITION (row 0 = ring 0, row 1 = ring 1, row 2 = ring 2),
|
|
120
|
+
* not by a "0 —"/"1 —"/"2 —" text prefix on the cell — the labels
|
|
121
|
+
* themselves carry no numeric prefix (round-3 fix: dropped per Issue #508),
|
|
122
|
+
* so a prefix match would no longer find anything. The 3-row length check
|
|
123
|
+
* above is what actually guarantees the ring-0/1/2 order; position is a
|
|
124
|
+
* reliable key once that's enforced. */
|
|
125
|
+
function ringLabels(enforcement: string): Record<0 | 1 | 2, string> {
|
|
126
|
+
const lines = enforcement.split('\n')
|
|
127
|
+
const summaryHeadingLine = findHeadingLine(lines, /^##\s*The model:/)
|
|
128
|
+
if (!summaryHeadingLine) throw new Error('enforcement.md: could not find "## The model:" heading')
|
|
129
|
+
const summaryTable = findTable(lines, summaryHeadingLine, false)
|
|
130
|
+
if (summaryTable?.rows.length !== 3) {
|
|
131
|
+
throw new Error('enforcement.md: summary table does not have exactly 3 rows (Ring 0/1/2)')
|
|
132
|
+
}
|
|
133
|
+
const byRow = summaryTable.rows.map((row) => (row.cells[0] ?? '').replace(/\*\*/g, '').trim())
|
|
134
|
+
return { 0: byRow[0] ?? '', 1: byRow[1] ?? '', 2: byRow[2] ?? '' }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
type RoleFrontmatter = {
|
|
138
|
+
roleId: string
|
|
139
|
+
title?: string
|
|
140
|
+
order?: number
|
|
141
|
+
description?: string
|
|
142
|
+
summary?: string
|
|
143
|
+
actorType?: 'agent' | 'human' | 'either'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function extractRole(content: string): RoleFrontmatter | null {
|
|
147
|
+
const { data } = matter(content)
|
|
148
|
+
if (typeof data.role_id !== 'string') return null
|
|
149
|
+
return {
|
|
150
|
+
roleId: data.role_id,
|
|
151
|
+
title: typeof data.title === 'string' ? data.title : undefined,
|
|
152
|
+
order: typeof data.order === 'number' ? data.order : undefined,
|
|
153
|
+
description: typeof data.description === 'string' ? data.description : undefined,
|
|
154
|
+
summary: typeof data.summary === 'string' ? data.summary : undefined,
|
|
155
|
+
actorType: data.actor === 'agent' || data.actor === 'human' || data.actor === 'either' ? data.actor : undefined
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
type ContractFrontmatter = {
|
|
160
|
+
contractId: string
|
|
161
|
+
title?: string
|
|
162
|
+
order?: number
|
|
163
|
+
description?: string
|
|
164
|
+
producer?: string
|
|
165
|
+
consumer?: string
|
|
166
|
+
summary?: string
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function extractContract(content: string): ContractFrontmatter | null {
|
|
170
|
+
const { data } = matter(content)
|
|
171
|
+
if (typeof data.contract_id !== 'string') return null
|
|
172
|
+
return {
|
|
173
|
+
contractId: data.contract_id,
|
|
174
|
+
title: typeof data.title === 'string' ? data.title : undefined,
|
|
175
|
+
order: typeof data.order === 'number' ? data.order : undefined,
|
|
176
|
+
description: typeof data.description === 'string' ? data.description : undefined,
|
|
177
|
+
producer: typeof data.producer === 'string' ? data.producer : undefined,
|
|
178
|
+
consumer: typeof data.consumer === 'string' ? data.consumer : undefined,
|
|
179
|
+
summary: typeof data.summary === 'string' ? data.summary : undefined
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The pure derivation. `doctrine` is already-read raw markdown; `config` is the
|
|
185
|
+
* declarative enable/disable input (null = nothing disabled); `tranche` is
|
|
186
|
+
* StateSource live context, passed through verbatim and never used to compute
|
|
187
|
+
* render-state.
|
|
188
|
+
*/
|
|
189
|
+
export function deriveDiagramModel(
|
|
190
|
+
doctrine: DoctrineContent,
|
|
191
|
+
config: DiagramConfig | null,
|
|
192
|
+
tranche: Tranche | null
|
|
193
|
+
): DiagramModel {
|
|
194
|
+
const nodes: DiagramNode[] = []
|
|
195
|
+
const edges: DiagramEdge[] = []
|
|
196
|
+
const findings: DiagramFinding[] = []
|
|
197
|
+
|
|
198
|
+
const labels = ringLabels(doctrine.enforcement)
|
|
199
|
+
const ring1Disabled = config?.rings?.ring1_forgeWriteInterception === false
|
|
200
|
+
const ring2Disabled = config?.rings?.ring2_asyncAudits === false
|
|
201
|
+
|
|
202
|
+
// --- Ring nodes (Ring 0 has no ring-level switch) ---
|
|
203
|
+
nodes.push({ id: 'ring:0', kind: 'ring', label: labels[0], ringIndex: 0, renderState: 'active' })
|
|
204
|
+
nodes.push({
|
|
205
|
+
id: 'ring:1',
|
|
206
|
+
kind: 'ring',
|
|
207
|
+
label: labels[1],
|
|
208
|
+
ringIndex: 1,
|
|
209
|
+
renderState: ring1Disabled ? 'disabled' : 'active'
|
|
210
|
+
})
|
|
211
|
+
nodes.push({
|
|
212
|
+
id: 'ring:2',
|
|
213
|
+
kind: 'ring',
|
|
214
|
+
label: labels[2],
|
|
215
|
+
ringIndex: 2,
|
|
216
|
+
renderState: ring2Disabled ? 'disabled' : 'active'
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// --- Gate (Ring 0) and check (Ring 1/2) nodes from the detail tables ---
|
|
220
|
+
const gateRows = parseEnforcementRegistry(doctrine.enforcement)
|
|
221
|
+
const usedSlugs = new Set<string>()
|
|
222
|
+
const gateCheckSlugs = new Set<string>()
|
|
223
|
+
// Track ring-0 gate nodes (label + id) for the `guards` keyword match.
|
|
224
|
+
const ring0Gates: Array<{ id: string; label: string }> = []
|
|
225
|
+
|
|
226
|
+
for (const row of gateRows) {
|
|
227
|
+
const ringIndex: 0 | 1 | 2 = row.ring === 'ring0' ? 0 : row.ring === 'ring1' ? 1 : 2
|
|
228
|
+
const kind: DiagramNodeKind = ringIndex === 0 ? 'gate' : 'check'
|
|
229
|
+
const base = slugify(row.action)
|
|
230
|
+
const slug = usedSlugs.has(base) ? `${base}-${row.line}` : base
|
|
231
|
+
usedSlugs.add(slug)
|
|
232
|
+
gateCheckSlugs.add(slug)
|
|
233
|
+
const id = `${kind}:${slug}`
|
|
234
|
+
|
|
235
|
+
let renderState: RenderState
|
|
236
|
+
if (config?.gates?.[slug] === false) {
|
|
237
|
+
renderState = 'disabled'
|
|
238
|
+
} else if (ringIndex === 1 && ring1Disabled) {
|
|
239
|
+
renderState = 'disabled'
|
|
240
|
+
} else if (ringIndex === 2 && ring2Disabled) {
|
|
241
|
+
renderState = 'disabled'
|
|
242
|
+
} else {
|
|
243
|
+
renderState = 'active'
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
nodes.push({
|
|
247
|
+
id,
|
|
248
|
+
kind,
|
|
249
|
+
label: row.action,
|
|
250
|
+
ringIndex,
|
|
251
|
+
renderState,
|
|
252
|
+
sourceLine: row.line,
|
|
253
|
+
summary: row.summary,
|
|
254
|
+
detail: row.description,
|
|
255
|
+
category: row.category
|
|
256
|
+
})
|
|
257
|
+
if (ringIndex === 0) ring0Gates.push({ id, label: row.action })
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// --- Action nodes (always active in v1) ---
|
|
261
|
+
for (const action of ACTIONS) {
|
|
262
|
+
nodes.push({
|
|
263
|
+
id: `action:${action.id}`,
|
|
264
|
+
kind: 'action',
|
|
265
|
+
label: action.label,
|
|
266
|
+
renderState: 'active',
|
|
267
|
+
summary: action.summary,
|
|
268
|
+
detail: action.description,
|
|
269
|
+
crosses: action.crosses
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// --- Role nodes from roles/*.md frontmatter (skip files without role_id) ---
|
|
274
|
+
const roleIds = new Set<string>()
|
|
275
|
+
for (const file of doctrine.roles) {
|
|
276
|
+
const role = extractRole(file.content)
|
|
277
|
+
if (role === null) continue
|
|
278
|
+
if (roleIds.has(role.roleId)) continue
|
|
279
|
+
roleIds.add(role.roleId)
|
|
280
|
+
nodes.push({
|
|
281
|
+
id: `role:${role.roleId}`,
|
|
282
|
+
kind: 'role',
|
|
283
|
+
label: role.roleId,
|
|
284
|
+
displayLabel: role.title,
|
|
285
|
+
order: role.order,
|
|
286
|
+
renderState: 'active',
|
|
287
|
+
summary: role.summary,
|
|
288
|
+
detail: role.description,
|
|
289
|
+
actorType: role.actorType
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// --- Contract nodes from contracts/*.md frontmatter (skip without id) ---
|
|
294
|
+
const contracts: ContractFrontmatter[] = []
|
|
295
|
+
for (const file of doctrine.contracts) {
|
|
296
|
+
const contract = extractContract(file.content)
|
|
297
|
+
if (contract === null) continue
|
|
298
|
+
contracts.push(contract)
|
|
299
|
+
nodes.push({
|
|
300
|
+
id: `contract:${contract.contractId}`,
|
|
301
|
+
kind: 'contract',
|
|
302
|
+
label: contract.contractId,
|
|
303
|
+
displayLabel: contract.title,
|
|
304
|
+
order: contract.order,
|
|
305
|
+
renderState: 'active',
|
|
306
|
+
summary: contract.summary,
|
|
307
|
+
detail: contract.description
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// --- Edges ---
|
|
312
|
+
// guards: ring-0 gate → into-github action, matched by CROSSING_KEYWORDS
|
|
313
|
+
// (case-insensitive substring on the gate row label; one edge per match).
|
|
314
|
+
for (const action of ACTIONS) {
|
|
315
|
+
if (action.crosses !== 'into-github') continue
|
|
316
|
+
const keyword = (CROSSING_KEYWORDS[action.id] ?? '').toLowerCase()
|
|
317
|
+
if (keyword === '') continue
|
|
318
|
+
for (const gate of ring0Gates) {
|
|
319
|
+
if (!gate.label.toLowerCase().includes(keyword)) continue
|
|
320
|
+
edges.push({
|
|
321
|
+
id: `guards:${gate.id}:action:${action.id}`,
|
|
322
|
+
kind: 'guards',
|
|
323
|
+
from: gate.id,
|
|
324
|
+
to: `action:${action.id}`
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// performs: role → action for every ACTIONS.performedBy entry (the canonical
|
|
330
|
+
// single source — the role's own `performs` frontmatter is intentionally not
|
|
331
|
+
// re-read, so the two lists can never drift).
|
|
332
|
+
for (const action of ACTIONS) {
|
|
333
|
+
for (const roleId of action.performedBy) {
|
|
334
|
+
edges.push({
|
|
335
|
+
id: `performs:role:${roleId}:action:${action.id}`,
|
|
336
|
+
kind: 'performs',
|
|
337
|
+
from: `role:${roleId}`,
|
|
338
|
+
to: `action:${action.id}`
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// produces / consumes: role → contract from each contract's producer/consumer.
|
|
344
|
+
for (const contract of contracts) {
|
|
345
|
+
if (contract.producer !== undefined) {
|
|
346
|
+
edges.push({
|
|
347
|
+
id: `produces:role:${contract.producer}:contract:${contract.contractId}`,
|
|
348
|
+
kind: 'produces',
|
|
349
|
+
from: `role:${contract.producer}`,
|
|
350
|
+
to: `contract:${contract.contractId}`
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
if (contract.consumer !== undefined) {
|
|
354
|
+
edges.push({
|
|
355
|
+
id: `consumes:role:${contract.consumer}:contract:${contract.contractId}`,
|
|
356
|
+
kind: 'consumes',
|
|
357
|
+
from: `role:${contract.consumer}`,
|
|
358
|
+
to: `contract:${contract.contractId}`
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// --- Findings: config keys that reference no real gate/check slug. Config
|
|
364
|
+
// references, never invents — surface the drift, don't silently drop it. ---
|
|
365
|
+
for (const key of Object.keys(config?.gates ?? {})) {
|
|
366
|
+
if (!gateCheckSlugs.has(key)) {
|
|
367
|
+
findings.push({ configKey: key, reason: `config gates["${key}"] references no gate or check in doctrine` })
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return { nodes, edges, findings, tranche }
|
|
372
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct-main-push detection (aeg-governance-hardening task 24, #364,
|
|
3
|
+
* Part 3 — item 1). Pure — no `fs`, no `gh`/`git` shell-outs. The CLI shim
|
|
4
|
+
* (`bin/check-direct-main-push.ts`) makes the one forge call (the
|
|
5
|
+
* commits→pulls association API: `GET /repos/{owner}/{repo}/commits/{sha}/pulls`,
|
|
6
|
+
* which GitHub documents as returning "the merged Pull Request that
|
|
7
|
+
* introduced the commit to the repository" for a commit on the default
|
|
8
|
+
* branch) and passes the result in here.
|
|
9
|
+
*
|
|
10
|
+
* Detection only, never prevention here: prevention lives at
|
|
11
|
+
* `.husky/pre-push` (ring 0) and the repository ruleset (ring 1). This
|
|
12
|
+
* predicate only decides whether the pushed commit is
|
|
13
|
+
* legitimate (associated with a merged PR) — the CLI shim opens an
|
|
14
|
+
* incident Issue and fails the run loudly on a violation; it never reverts
|
|
15
|
+
* or mutates anything (Principal decision, brief §11).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type DirectMainPushFact = {
|
|
19
|
+
sha: string
|
|
20
|
+
/** PR numbers the commits→pulls association API reports as MERGED for this commit. Empty when the commit has no associated merged PR — i.e. it was pushed directly. */
|
|
21
|
+
associatedMergedPrNumbers: number[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type DirectMainPushResult =
|
|
25
|
+
| { verdict: 'legitimate'; mergedPrNumber: number }
|
|
26
|
+
| { verdict: 'direct-push'; sha: string }
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `legitimate` — at least one merged PR is associated with this commit (the
|
|
30
|
+
* normal case: a squash-merge or merge commit that landed via a PR).
|
|
31
|
+
* `direct-push` — no associated merged PR at all. This is the violation:
|
|
32
|
+
* either a genuine direct push, or (equivalently suspicious) a commit the
|
|
33
|
+
* association API cannot explain.
|
|
34
|
+
*/
|
|
35
|
+
export function checkDirectMainPush(fact: DirectMainPushFact): DirectMainPushResult {
|
|
36
|
+
const [first] = fact.associatedMergedPrNumbers
|
|
37
|
+
if (first !== undefined) return { verdict: 'legitimate', mergedPrNumber: first }
|
|
38
|
+
return { verdict: 'direct-push', sha: fact.sha }
|
|
39
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatch-readiness composition (aeg-governance-hardening task 11, #324).
|
|
3
|
+
* Pure — no `fs`, no `fetch`. Composes the forge/rationale/provenance/
|
|
4
|
+
* archival facts the CLI shim (`bin/verify-dispatch.ts`) gathers (reusing
|
|
5
|
+
* `parseTranche`, `hasProvenance`, `taskRefFromBranch`,
|
|
6
|
+
* `checkIssueRationale`, `fetchProvenance` — never re-implementing any of
|
|
7
|
+
* them) into one `{ ready, blockers }` verdict, one blocker string per
|
|
8
|
+
* failing predicate.
|
|
9
|
+
*
|
|
10
|
+
* Every predicate here mechanizes a prose precondition that `roles/
|
|
11
|
+
* developer.md`'s entry gate and `contracts/brief-developer.md` currently
|
|
12
|
+
* state as something the Developer must re-derive by hand — the exact gap
|
|
13
|
+
* this task exists to close: four Developer agents independently re-derived
|
|
14
|
+
* (and stopped on) the same archival fact from scratch, at real token cost,
|
|
15
|
+
* hours after it first became true (aeg-governance-hardening's 2026-07-02/03
|
|
16
|
+
* live-fire wave).
|
|
17
|
+
*
|
|
18
|
+
* Message style mirrors `coherence-checks.ts`'s A1/A2/T2/etc. family: one
|
|
19
|
+
* line per failure, naming the exact task/Issue/PR involved.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { Task } from './types'
|
|
23
|
+
import { isPrincipal, PRINCIPAL_ALLOWLIST } from './waiver-label'
|
|
24
|
+
|
|
25
|
+
export type DispatchIssueFact = { number: number; state: 'open' | 'closed' } | null
|
|
26
|
+
|
|
27
|
+
export type DispatchEdgeFact = {
|
|
28
|
+
/** The edge's target task id, as written in the topology's depends-on/conflicts-with cell. */
|
|
29
|
+
id: string
|
|
30
|
+
/** The target task's Issue number, if known — for message readability only. */
|
|
31
|
+
issue: number | null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type DispatchDependsOnFact = DispatchEdgeFact & {
|
|
35
|
+
merged: boolean
|
|
36
|
+
/**
|
|
37
|
+
* `false` when the resolver could not find any tranche/task/Issue matching
|
|
38
|
+
* this edge at all — a fact distinct from `merged: false`, which means the
|
|
39
|
+
* edge resolved to a real target that just hasn't merged yet. Conflating
|
|
40
|
+
* the two produced a false "not merged yet" claim for #193's
|
|
41
|
+
* `vinaya-milestone-model-v1 2` edge, which had genuinely already merged
|
|
42
|
+
* (#196) — the message this distinction exists to correct. Absent or
|
|
43
|
+
* `true` for every edge the resolver actually matched to a target,
|
|
44
|
+
* including one whose target lookup itself then failed (an outage), which
|
|
45
|
+
* stays under the existing conservative `merged: false` default rather
|
|
46
|
+
* than this one.
|
|
47
|
+
*/
|
|
48
|
+
resolved?: boolean
|
|
49
|
+
/**
|
|
50
|
+
* Hand-close recognition facts (task `vinaya-engine-v1` 21, #99) — a
|
|
51
|
+
* second, narrower path alongside `merged` for a dependency Issue closed
|
|
52
|
+
* directly by a recognized Principal, with a stated `COMPLETED` reason,
|
|
53
|
+
* rather than via a merged PR. All three are `null`/absent when the edge
|
|
54
|
+
* is unresolvable or the target issue is open/has no close event — in
|
|
55
|
+
* which case this path never fires and only `merged` matters, same as
|
|
56
|
+
* before this task.
|
|
57
|
+
*/
|
|
58
|
+
issueState?: 'open' | 'closed' | null
|
|
59
|
+
stateReason?: 'completed' | 'not_planned' | null
|
|
60
|
+
closedByActor?: string | null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* True when a dependency Issue was closed directly by a recognized Principal
|
|
65
|
+
* with a stated `COMPLETED` reason — the second, narrower "done" path this
|
|
66
|
+
* task adds alongside `merged`. Deliberately conjunctive and centralized
|
|
67
|
+
* here (not duplicated per caller): the trap this task exists to avoid is
|
|
68
|
+
* "any closed Issue with a comment counts," so every condition below is a
|
|
69
|
+
* real forge fact, not a prose claim —
|
|
70
|
+
* - `issueState === 'closed'`: the Issue is actually closed.
|
|
71
|
+
* - `stateReason === 'completed'`: GitHub's own close reason says resolved,
|
|
72
|
+
* not `not_planned` (abandoned — the opposite of resolved).
|
|
73
|
+
* - `closedByActor` is the GitHub login that performed the CLOSED_EVENT
|
|
74
|
+
* (not a claim in a comment body) and is a member of the recognized
|
|
75
|
+
* Principal allowlist.
|
|
76
|
+
*/
|
|
77
|
+
function isHandClosedByRecognizedPrincipal(
|
|
78
|
+
dep: Pick<DispatchDependsOnFact, 'issueState' | 'stateReason' | 'closedByActor'>,
|
|
79
|
+
principalAllowlist: string[]
|
|
80
|
+
): boolean {
|
|
81
|
+
return (
|
|
82
|
+
dep.issueState === 'closed' &&
|
|
83
|
+
dep.stateReason === 'completed' &&
|
|
84
|
+
isPrincipal(dep.closedByActor ?? null, principalAllowlist)
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type DispatchConflictsWithFact = DispatchEdgeFact & { openOrInFlight: boolean }
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Removed the predicate that consumed this fact
|
|
92
|
+
* (`checkDispatchReadiness` no longer blocks on it) — automated the
|
|
93
|
+
* signal (per-task provenance posting) this predicate existed to protect,
|
|
94
|
+
* making the row-adjacency gate itself the stale part, not the automation.
|
|
95
|
+
* Kept, dormant, rather than deleted: several callers (`bin/verify-dispatch.ts`,
|
|
96
|
+
* both Studio/Vinaya `map-dispatch-input.ts` mappers) still assemble this
|
|
97
|
+
* fact as harmless dead plumbing, and `DispatchGateInput.priorTask` below
|
|
98
|
+
* remains the exact shape needed to test the predicate's absence.
|
|
99
|
+
*/
|
|
100
|
+
export type DispatchPriorTaskFact = {
|
|
101
|
+
id: string
|
|
102
|
+
issue: number | null
|
|
103
|
+
issueClosed: boolean
|
|
104
|
+
prMerged: boolean
|
|
105
|
+
hasProvenance: boolean
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type DispatchPriorTrancheFact = {
|
|
109
|
+
project: string
|
|
110
|
+
/** null when this project has no prior tranche at all — the gate passes trivially for it. */
|
|
111
|
+
priorTrancheSlug: string | null
|
|
112
|
+
archived: boolean
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type DispatchGateInput = {
|
|
116
|
+
trancheSlug: string
|
|
117
|
+
/** The topology row for this task, as parsed by `parseTranche`. */
|
|
118
|
+
task: Task
|
|
119
|
+
/** null when the task has no Issue (#TBD/blank) OR the Issue number doesn't resolve on the forge (phantom ref). */
|
|
120
|
+
issue: DispatchIssueFact
|
|
121
|
+
/** `checkIssueRationale` result for this task's Issue — irrelevant (treated as passing) when `issue` is null. */
|
|
122
|
+
issueRationalePass: boolean
|
|
123
|
+
dependsOn: DispatchDependsOnFact[]
|
|
124
|
+
conflictsWith: DispatchConflictsWithFact[]
|
|
125
|
+
/**
|
|
126
|
+
* The immediately-prior task in this same tranche's topology, or null
|
|
127
|
+
* when this is the first task. no longer read by
|
|
128
|
+
* `checkDispatchReadiness` — dormant field, kept for caller compatibility.
|
|
129
|
+
*/
|
|
130
|
+
priorTask: DispatchPriorTaskFact | null
|
|
131
|
+
/** One entry per project named in `task.projects`. */
|
|
132
|
+
priorTrancheArchival: DispatchPriorTrancheFact[]
|
|
133
|
+
/**
|
|
134
|
+
* Overrides `PRINCIPAL_ALLOWLIST` for hand-close recognition when
|
|
135
|
+
* provided — an adopter repo's own `vinaya.config.json` `principals`
|
|
136
|
+
* field, resolved by the CLI bin before calling in (never read from here;
|
|
137
|
+
* this stays pure). Defaults to `PRINCIPAL_ALLOWLIST` when omitted, same
|
|
138
|
+
* pattern as `checkReviewGate`'s `principalAllowlist` (`review-gate.ts`) —
|
|
139
|
+
* hardcoding this repo's own principal made that gate unpassable on any
|
|
140
|
+
* adopter repo; the same mistake here would silently do the same to the
|
|
141
|
+
* hand-close path.
|
|
142
|
+
*/
|
|
143
|
+
principalAllowlist?: string[]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export type DispatchResult = { ready: boolean; blockers: string[] }
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* True when a parsed `depends-on` edge points back at its own host task.
|
|
150
|
+
*
|
|
151
|
+
* A self-dependency is unsatisfiable by construction, so it is never a real
|
|
152
|
+
* gate state — its presence is proof of a defect in the edge text or in its
|
|
153
|
+
* resolution. `parseRationaleDeps` (`@attalabs/aeg-forge-state`) reads only
|
|
154
|
+
* a labeled `Depends-on:`/`Conflicts-with:` field's own comma-separated,
|
|
155
|
+
* id-shaped tokens; since Issue #347 an unlabeled/bare span elsewhere in the
|
|
156
|
+
* "Dependency rationale" section is never read as an edge, whatever its
|
|
157
|
+
* shape (`parse-rationale-deps.ts`'s own module comment states the rule). A
|
|
158
|
+
* slug-qualified token in that list (`aeg-governance-hardening #368`) still
|
|
159
|
+
* resolves its trailing bare number against the NAMED tranche, not the host
|
|
160
|
+
* — a resolver bug there is what would land a task depending on itself, on
|
|
161
|
+
* that tranche's own task `1`.
|
|
162
|
+
*
|
|
163
|
+
* Reported as an INTERNAL error rather than through the ordinary
|
|
164
|
+
* "not merged yet" branch below, because that message reads as a legitimate
|
|
165
|
+
* serialization and invites the reader to route around it: two Developer
|
|
166
|
+
* agents faced with exactly this gate concluded it was a false positive and
|
|
167
|
+
* committed with `--no-verify` to get past their hooks. Naming it a tool bug
|
|
168
|
+
* converts "agent improvises around a nonsense gate" into "agent escalates,"
|
|
169
|
+
* which is the behavior the gate architecture assumes.
|
|
170
|
+
*
|
|
171
|
+
* Two arms, because the edge can carry either shape: a resolved Issue number
|
|
172
|
+
* equal to this task's own, or — the live case — a bare same-tranche id equal
|
|
173
|
+
* to this task's own id.
|
|
174
|
+
*/
|
|
175
|
+
function isSelfDependency(dep: DispatchDependsOnFact, task: Task, issue: DispatchIssueFact): boolean {
|
|
176
|
+
if (dep.issue !== null && issue !== null && dep.issue === issue.number) return true
|
|
177
|
+
const bare = dep.id.trim().replace(/^#/, '')
|
|
178
|
+
return bare.toLowerCase() === task.id.trim().toLowerCase()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function checkDispatchReadiness(input: DispatchGateInput): DispatchResult {
|
|
182
|
+
const { trancheSlug, task } = input
|
|
183
|
+
const taskLabel = `task ${task.id} (tranche ${trancheSlug})`
|
|
184
|
+
const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST
|
|
185
|
+
const blockers: string[] = []
|
|
186
|
+
|
|
187
|
+
// Issue-existence — the topology row itself has no Issue number.
|
|
188
|
+
if (task.issue === null) {
|
|
189
|
+
blockers.push(
|
|
190
|
+
`dispatch-gate issue-existence: ${taskLabel} has no Issue (#TBD or blank) in the topology — not dispatchable until the Planner cuts the Issue.`
|
|
191
|
+
)
|
|
192
|
+
} else if (input.issue === null) {
|
|
193
|
+
// Row names an Issue number, but it doesn't resolve on the forge — phantom ref (T1's fail class).
|
|
194
|
+
blockers.push(
|
|
195
|
+
`dispatch-gate issue-existence: ${taskLabel} names Issue #${task.issue}, but it does not resolve to a real GitHub Issue (phantom reference).`
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Planner-rationale completeness — only evaluable when the Issue itself resolved.
|
|
200
|
+
if (input.issue !== null && !input.issueRationalePass) {
|
|
201
|
+
blockers.push(
|
|
202
|
+
`dispatch-gate rationale: Issue #${input.issue.number} for ${taskLabel} fails the rationale gate (checkIssueRationale) — the Planner must complete the eight-field rationale before this task is dispatchable.`
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Depends-on merged — OR hand-closed by a recognized Principal (task
|
|
207
|
+
// `vinaya-engine-v1` 21, #99): a second, narrower path for a dependency
|
|
208
|
+
// Issue closed directly rather than via a merged PR.
|
|
209
|
+
for (const dep of input.dependsOn) {
|
|
210
|
+
// Evaluated BEFORE the unresolvable branch: a self-reference that also
|
|
211
|
+
// failed to resolve is still a parser bug, and reporting it as an
|
|
212
|
+
// unresolvable edge would send the reader to correct edge text that is
|
|
213
|
+
// not actually the fault.
|
|
214
|
+
if (isSelfDependency(dep, task, input.issue)) {
|
|
215
|
+
const issueStr = input.issue !== null ? `#${input.issue.number}` : '?'
|
|
216
|
+
blockers.push(
|
|
217
|
+
`dispatch-gate INTERNAL: parsed a self-dependency for task ${task.id} (${issueStr}) — this is a parser bug in parseRationaleDeps, not a real dependency. Please report it upstream. Re-run once the rationale is corrected or the fix ships.`
|
|
218
|
+
)
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
if (dep.resolved === false) {
|
|
222
|
+
// Distinct from the "not merged yet" branch below (#196): this edge
|
|
223
|
+
// never resolved to any tranche/task/Issue at all, so a "not merged"
|
|
224
|
+
// claim would misattribute the failure to the forge rather than to
|
|
225
|
+
// the edge text. Still blocks — the conservative default is correct
|
|
226
|
+
// for a genuinely unresolvable edge — but says so honestly.
|
|
227
|
+
blockers.push(
|
|
228
|
+
`dispatch-gate depends-on: ${taskLabel} depends on "${dep.id}", which is UNRESOLVABLE — the resolver could not find a matching tranche/task/Issue for this edge (not a claim about merge status). Not dispatchable until the edge is corrected.`
|
|
229
|
+
)
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
if (!dep.merged && !isHandClosedByRecognizedPrincipal(dep, principalAllowlist)) {
|
|
233
|
+
const issueStr = dep.issue !== null ? ` (#${dep.issue})` : ''
|
|
234
|
+
blockers.push(
|
|
235
|
+
`dispatch-gate depends-on: ${taskLabel} depends on ${dep.id}${issueStr}, whose PR is not merged yet — not dispatchable, it serializes behind it.`
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Conflicts-with not open/in-flight.
|
|
241
|
+
for (const c of input.conflictsWith) {
|
|
242
|
+
if (c.openOrInFlight) {
|
|
243
|
+
const issueStr = c.issue !== null ? ` (#${c.issue})` : ''
|
|
244
|
+
blockers.push(
|
|
245
|
+
`dispatch-gate conflicts-with: ${taskLabel} conflicts with ${c.id}${issueStr}, whose PR is open or in-flight — not dispatchable until it merges.`
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Prior-task archival / row-adjacency predicate REMOVED (2026-07-13).
|
|
251
|
+
// The retired predicate required every earlier table row's full archival (Issue
|
|
252
|
+
// closed, PR merged, provenance posted) regardless of whether that row was
|
|
253
|
+
// a declared dependency automated the provenance-posting signal
|
|
254
|
+
// this existed to protect, so the blanket row-order block outlived its
|
|
255
|
+
// justification. `input.priorTask` is still accepted (dormant) for caller
|
|
256
|
+
// compatibility; see the type's doc comment above.
|
|
257
|
+
|
|
258
|
+
// Prior-tranche archival, per project named in Project(s).
|
|
259
|
+
for (const proj of input.priorTrancheArchival) {
|
|
260
|
+
if (proj.priorTrancheSlug !== null && !proj.archived) {
|
|
261
|
+
blockers.push(
|
|
262
|
+
`dispatch-gate prior-tranche-archival: project \`${proj.project}\`'s previous tranche \`${proj.priorTrancheSlug}\` is not archived — the Tranche Archivist must run before new work on this product.`
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return { ready: blockers.length === 0, blockers }
|
|
268
|
+
}
|