@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,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed model for AEG artifacts. See `aeg-root/tranche-model.md` for the
|
|
3
|
+
* canonical specification (§3 status table, §4 thin-file template).
|
|
4
|
+
*
|
|
5
|
+
* This module is pure: no I/O. The parser produces these shapes from file
|
|
6
|
+
* contents; `deriveTranche` consumes them plus a `ForgeFacts` snapshot.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ForgeFacts, Tranche, Lifecycle, Task } from '@attalabs/aeg-types'
|
|
10
|
+
|
|
11
|
+
// ---------- Registry (projects.md) ----------
|
|
12
|
+
|
|
13
|
+
export type Project = {
|
|
14
|
+
/** Registry-unique name. Authority for `Project:` validation. */
|
|
15
|
+
name: string
|
|
16
|
+
/** Declared home folder (e.g. `apps/<product>`). Never derived. */
|
|
17
|
+
path: string
|
|
18
|
+
/** Specs folder for the project. */
|
|
19
|
+
specsPath: string
|
|
20
|
+
/**
|
|
21
|
+
* Per-project state folder, or `null` when the cell holds prose like
|
|
22
|
+
* "state tracked globally for now" — i.e. state isn't tracked under this
|
|
23
|
+
* project's path.
|
|
24
|
+
*/
|
|
25
|
+
statePath: string | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type Registry = Project[]
|
|
29
|
+
|
|
30
|
+
// ---------- Tranche file (tranches/<name>.md) ----------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `Lifecycle`/`Task`/`Tranche` live in `@attalabs/aeg-types` (task
|
|
34
|
+
* aeg-forge-state-v1 3a) — re-exported here since every existing call site
|
|
35
|
+
* across the repo imports them from `@attalabs/aeg-core`. Moved out so
|
|
36
|
+
* `@attalabs/aeg-forge-state` can depend on these shapes without creating a
|
|
37
|
+
* package cycle with `aeg-core` (which in turn depends on
|
|
38
|
+
* `aeg-forge-state`'s derivation function).
|
|
39
|
+
*/
|
|
40
|
+
export type { Lifecycle, Task, Tranche }
|
|
41
|
+
|
|
42
|
+
// ---------- Forge facts ----------
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* `ForgeFacts` lives in `@attalabs/aeg-types` (aeg-core-purity fix, #521) —
|
|
46
|
+
* re-exported here for the same reason as `Lifecycle`/`Task`/`Tranche`:
|
|
47
|
+
* every existing call site imports it from `@attalabs/aeg-core`, and it moved
|
|
48
|
+
* out so the I/O-performing fetchers that produce it (now in
|
|
49
|
+
* `@attalabs/aeg-forge-state`) don't need to depend on `aeg-core`.
|
|
50
|
+
*/
|
|
51
|
+
export type { ForgeFacts }
|
|
52
|
+
|
|
53
|
+
// ---------- Derivation output ----------
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The statuses derivation can conclude. The *rules* that produce them are not
|
|
57
|
+
* written here or in `derive-tranche.ts` — they live as an ordered,
|
|
58
|
+
* pure-data list in `state-machine-model.ts` (the `actions.ts`
|
|
59
|
+
* discipline), which `deriveStatus` executes and the docs render from the
|
|
60
|
+
* same list.
|
|
61
|
+
*
|
|
62
|
+
* `backlog` remains a member because it is a project-level concept consumers
|
|
63
|
+
* still render; derivation never emits it inside a tranche.
|
|
64
|
+
*
|
|
65
|
+
* `dropped` and `incoherent` are the two **honest terminal** statuses.
|
|
66
|
+
* A closed Issue with no merged PR must never resolve to `todo` (which implies
|
|
67
|
+
* not-started). `dropped` = closed `NOT_PLANNED` (legitimately abandoned);
|
|
68
|
+
* `incoherent` = closed `COMPLETED` but with no merged-PR link (genuinely done
|
|
69
|
+
* yet unprovable, or a broken close). Both are surfaced, never hidden.
|
|
70
|
+
*/
|
|
71
|
+
export type DerivedStatus =
|
|
72
|
+
| 'backlog'
|
|
73
|
+
| 'todo'
|
|
74
|
+
| 'in-flight'
|
|
75
|
+
| 'in-review'
|
|
76
|
+
| 'changes-requested'
|
|
77
|
+
| 'merged'
|
|
78
|
+
| 'blocked'
|
|
79
|
+
| 'dropped'
|
|
80
|
+
| 'incoherent'
|
|
81
|
+
|
|
82
|
+
export type DispatchBlockers = {
|
|
83
|
+
/** Depends-on edges whose target is not yet `merged`. */
|
|
84
|
+
dependsOnNotMerged: string[]
|
|
85
|
+
/**
|
|
86
|
+
* Conflicts-with edges whose target is currently occupying its collision
|
|
87
|
+
* domain — derived status `in-flight`, `in-review`, or `changes-requested`
|
|
88
|
+
* (§8 conflict gate).
|
|
89
|
+
*/
|
|
90
|
+
conflictsWithOpenOrInFlight: string[]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type DerivedTask = {
|
|
94
|
+
task: Task
|
|
95
|
+
status: DerivedStatus
|
|
96
|
+
/**
|
|
97
|
+
* `true` when the §8 gates allow dispatching this task: all `dependsOn`
|
|
98
|
+
* targets are `merged`, and no `conflictsWith` target is open/in-flight.
|
|
99
|
+
* Dispatchability is independent of this task's own current status — a
|
|
100
|
+
* task already `in-review` is still "dispatchable" by this definition (the
|
|
101
|
+
* gates would have allowed it to start).
|
|
102
|
+
*/
|
|
103
|
+
dispatchable: boolean
|
|
104
|
+
blockers: DispatchBlockers
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type UnknownEdge = {
|
|
108
|
+
from: string
|
|
109
|
+
to: string
|
|
110
|
+
kind: 'depends-on' | 'conflicts-with'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type DerivedTranche = {
|
|
114
|
+
tranche: Tranche
|
|
115
|
+
tasks: DerivedTask[]
|
|
116
|
+
/**
|
|
117
|
+
* Edges that reference task ids not present in this tranche's topology
|
|
118
|
+
* table. Surface for diagnostics; does not throw, since real files
|
|
119
|
+
* occasionally name dropped/prose-only ids (e.g. a `3a` that survives only
|
|
120
|
+
* in a tranche file's narrative).
|
|
121
|
+
*/
|
|
122
|
+
unknownEdges: UnknownEdge[]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------- Token ledger (tranches/<name>.tokens.md) ----------
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* One row of the append-only per-tranche token/cost ledger. See
|
|
129
|
+
* `aeg-root/tranche-model.md` §12 for the canonical format. Each role
|
|
130
|
+
* appends one row at the end of its turn; re-entry appends another row.
|
|
131
|
+
* The tranche total is `sum(rows)`, derived at read time — never stored.
|
|
132
|
+
*
|
|
133
|
+
* The two capture sources surface here as `null` cells: a row written by a
|
|
134
|
+
* claude.ai role (Planner / Brief Author / Reviewer / Security) before the
|
|
135
|
+
* Principal has filled in its usage figure has `tokensIn`/`tokensOut`/`cost`
|
|
136
|
+
* as `null`; a terminal-role row (Developer / Archivist with `/cost`) has
|
|
137
|
+
* exact numbers.
|
|
138
|
+
*/
|
|
139
|
+
export type LedgerRow = {
|
|
140
|
+
/** Free-text phase identifier. Convention: `<task-id>: <phase>` per task
|
|
141
|
+
* (e.g. `9: develop`), or a bare phase for tranche-wide work (e.g.
|
|
142
|
+
* `planning`). The parser does not enforce the convention — the Phase
|
|
143
|
+
* cell is opaque so the table can be re-pivoted later. */
|
|
144
|
+
phase: string
|
|
145
|
+
role: string
|
|
146
|
+
/** Free-text — the role's agent + model (e.g. `claude-opus-4-7 (CC)`,
|
|
147
|
+
* `claude-opus-4-7 (chat)`). The PRICING table dependency is on the
|
|
148
|
+
* model name, not on the parser. */
|
|
149
|
+
agentModel: string
|
|
150
|
+
/** `null` when the cell was `—` / `-` / empty (unknown — claude.ai
|
|
151
|
+
* row not yet filled). */
|
|
152
|
+
tokensIn: number | null
|
|
153
|
+
tokensOut: number | null
|
|
154
|
+
/** Cost in USD as a number. `null` for unknown. Cost depends on the
|
|
155
|
+
* adapter's PRICING table — when the model is missing from it, this is
|
|
156
|
+
* `0` from the source row, not `null` (a known limitation, not a parser
|
|
157
|
+
* bug). */
|
|
158
|
+
cost: number | null
|
|
159
|
+
/** Raw date cell — typically `YYYY-MM-DD`. Captured as a string so the
|
|
160
|
+
* model docs can evolve the format without breaking the parser. */
|
|
161
|
+
date: string
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* `sumLedger(rows)` output — pure aggregate over the rows in source order.
|
|
166
|
+
* Null cells contribute zero; the row count is preserved so a UI can show
|
|
167
|
+
* "N entries totalling …".
|
|
168
|
+
*/
|
|
169
|
+
export type LedgerTotals = {
|
|
170
|
+
tokensIn: number
|
|
171
|
+
tokensOut: number
|
|
172
|
+
cost: number
|
|
173
|
+
rows: number
|
|
174
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared code-reviewer / security-review verdict extraction (aeg-review-gate-v1
|
|
3
|
+
* task 1, #474). Originally private to `archive-task.ts`'s post-merge
|
|
4
|
+
* provenance assembly (advisory-only, DANGLING on a missing/unclear verdict).
|
|
5
|
+
* Extracted so the pre-merge `review-gate.ts` blocking check calls the
|
|
6
|
+
* IDENTICAL regex/parsing logic — a future drift between two copies of the
|
|
7
|
+
* same pattern would silently reopen the gap this task exists to close (one
|
|
8
|
+
* implementation per fact, §11 constraint). Pure — no `fs`, no `fetch`.
|
|
9
|
+
*
|
|
10
|
+
* Picks the MOST RECENT comment carrying a clear value, not the first comment
|
|
11
|
+
* merely matching the pattern (aeg-review-gate-v1 task 1 fix — the original
|
|
12
|
+
* single-comment `.find()` broke on real multi-comment PRs: a REQUEST_CHANGES
|
|
13
|
+
* verdict followed by fixes and a later clean APPROVE).
|
|
14
|
+
*
|
|
15
|
+
* Line-anchored `VERDICT:` marker only — NOT a bare word search anywhere in
|
|
16
|
+
* the comment (aeg-review-gate-v1 task 1 follow-up, security-review FAIL
|
|
17
|
+
* finding, confirmed by direct execution: the old bare `\b(PASS|FAIL)\b`
|
|
18
|
+
* pattern matched the post-merge Archivist's own auto-generated DANGLING
|
|
19
|
+
* placeholder text — "no security-review pass was run before merge —
|
|
20
|
+
* DANGLING, see below" — as a clean PASS, since it contains the standalone
|
|
21
|
+
* word "pass". Same risk in reverse for "APPROVE" inside ordinary prose,
|
|
22
|
+
* e.g. "I do NOT approve of that design"). `^\s*VERDICT:\s*<value>\b`,
|
|
23
|
+
* multiline, is the real subagent contract (`roles/reviewer.md`/
|
|
24
|
+
* `roles/security.md`'s own VERDICT block, which `.claude/agents/
|
|
25
|
+
* code-reviewer.md`/`security-reviewer.md` require verbatim) — tightening
|
|
26
|
+
* to it closes the gap without inventing a new convention.
|
|
27
|
+
*
|
|
28
|
+
* The VALUE is windowed to the comment's first THREE lines only
|
|
29
|
+
* (review-convergence-v1 task 2, round 4, `#392`) — never a line anywhere
|
|
30
|
+
* else in the body. Every shape this package's own callers render puts the
|
|
31
|
+
* `VERDICT:`/`ESCALATE:` line first and `Judged head:` third; a
|
|
32
|
+
* caller-supplied field (findings, conformance prose, a summary) never
|
|
33
|
+
* renders before line 5. Restricting the read window closes a
|
|
34
|
+
* caller-controlled-text injection route without narrowing what any real
|
|
35
|
+
* render needs matched.
|
|
36
|
+
*
|
|
37
|
+
* The CANDIDATE SET stays whole-body (review-convergence-v1 task 2, round 5,
|
|
38
|
+
* `#392`) — round 4 windowed where a verdict's value is read, not which
|
|
39
|
+
* comments count as a candidate at all. A comment whose only VERDICT-shaped
|
|
40
|
+
* line sits outside its first three lines is still the most recent
|
|
41
|
+
* candidate if it is the most recent comment; it reads as DANGLING rather
|
|
42
|
+
* than being skipped, so it correctly shadows an earlier clean verdict
|
|
43
|
+
* instead of silently letting the earlier one win.
|
|
44
|
+
*
|
|
45
|
+
* The anchor tolerates a leading markdown EMPHASIS run — one to three `*` or
|
|
46
|
+
* `_`, immediately abutting the token — so `**VERDICT: APPROVE**` and
|
|
47
|
+
* `_VERDICT: APPROVE_` match (PR #636: the reviewer subagent emitted the
|
|
48
|
+
* bolded form and the gate read the PR as carrying no code-review verdict at
|
|
49
|
+
* all). The spec still mandates the bare line; this only stops an agent's
|
|
50
|
+
* markdown drift from silently DANGLING a real verdict.
|
|
51
|
+
*
|
|
52
|
+
* Emphasis ONLY — the following are deliberately NOT tolerated, because each
|
|
53
|
+
* is a way for prose to *mention* a verdict rather than *cast* one, and this
|
|
54
|
+
* gate blocks merges (#639 review, findings 1/3/5):
|
|
55
|
+
* `> VERDICT: APPROVE` blockquote — GitHub's quote-reply syntax. A
|
|
56
|
+
* Developer quoting the reviewer's earlier text
|
|
57
|
+
* would otherwise become the PR's own verdict, and
|
|
58
|
+
* most-recent-hit-wins means the quote beats a live
|
|
59
|
+
* REQUEST CHANGES. Fail-open and silent.
|
|
60
|
+
* `* VERDICT: APPROVE` list item — the space is what distinguishes it
|
|
61
|
+
* from emphasis; a bullet is prose context. `-` and
|
|
62
|
+
* `1.` never matched, so `*` must not either.
|
|
63
|
+
* `# VERDICT: APPROVE` heading — same mention-not-cast reasoning.
|
|
64
|
+
* `` `VERDICT: APPROVE` `` code span — decided, not overlooked. A
|
|
65
|
+
* backticked marker is exactly how the role docs
|
|
66
|
+
* and this comment WRITE about the contract, so
|
|
67
|
+
* tolerating it would match prose describing the
|
|
68
|
+
* rule. If a real agent ever emits the backticked
|
|
69
|
+
* form, fix the agent: it fails loud (DANGLING),
|
|
70
|
+
* which is the safe direction.
|
|
71
|
+
*
|
|
72
|
+
* The value-side boundary is `(?![A-Za-z0-9])`, not `\b`: `_` is a word
|
|
73
|
+
* character, so `\b` after the captured value would reject the closing `_` of
|
|
74
|
+
* `_VERDICT: APPROVE_` while accepting the `*` of the bolded form — the class
|
|
75
|
+
* and the claim would disagree. It still rejects `APPROVED`/`PASSED`.
|
|
76
|
+
*
|
|
77
|
+
* What is NOT loosened is the requirement that a literal `VERDICT:` token be
|
|
78
|
+
* present — ordinary prose and the Archivist's own DANGLING placeholder still
|
|
79
|
+
* miss.
|
|
80
|
+
*
|
|
81
|
+
* No separate "marker present but value unclear" branch: a generic
|
|
82
|
+
* `VERDICT:`-prefix-only marker would itself cross-contaminate the two
|
|
83
|
+
* extractors (a security reviewer's own `VERDICT: PASS` line would loosely
|
|
84
|
+
* match the code-review marker too, misreporting "code-reviewer comment
|
|
85
|
+
* found but unclear" for a comment that was never a code review at all).
|
|
86
|
+
* Anything that doesn't match the exact value pattern reads as "missing",
|
|
87
|
+
* identical to no comment existing — which is also what the DANGLING
|
|
88
|
+
* placeholder case above requires.
|
|
89
|
+
*
|
|
90
|
+
* Reviewed-commit binding (#73, a duplicate of #71 closes this one). The
|
|
91
|
+
* winning comment (the one whose `VERDICT:` line matched) is also searched
|
|
92
|
+
* for a `Judged head: <sha>` line — the exact phrase reviewers on
|
|
93
|
+
* atta-labs/attalabs#664 were already hand-typing before this existed as a
|
|
94
|
+
* mechanism, formalized rather than invented. Same anchor discipline as
|
|
95
|
+
* `VERDICT:` itself: line-start, optional leading emphasis run, no
|
|
96
|
+
* blockquote/list-item/heading/code-span tolerance — a sha mentioned in
|
|
97
|
+
* ordinary prose ("see 8365ca57 for context") is a mention, not a binding,
|
|
98
|
+
* and must not parse as one. Accepts both the abbreviated (7-char) and full
|
|
99
|
+
* (40-char) hex forms; `review-gate.ts` does the prefix comparison against
|
|
100
|
+
* the PR's actual head. Searched only within the SAME comment body that
|
|
101
|
+
* produced the winning verdict — a sha mentioned in a different comment is
|
|
102
|
+
* not this verdict's binding.
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
const HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Round-4 ruling on `#392`: both markers are read from a comment's first
|
|
109
|
+
* THREE lines only, never anywhere else in the body. Every shape this
|
|
110
|
+
* package itself renders (`review-post.ts`'s `renderCodeReviewComment`/
|
|
111
|
+
* `renderSecurityComment`/`renderEscalationComment`) puts `VERDICT:`/
|
|
112
|
+
* `ESCALATE:` on line 1 and `Judged head:` on line 3 — every caller-supplied
|
|
113
|
+
* field (findings, conformance prose, scope, a summary) renders strictly
|
|
114
|
+
* after that, starting at line 5 at the earliest. Restricting the window to
|
|
115
|
+
* lines 1–3 costs no real render anything: it is a strictly narrower read
|
|
116
|
+
* than "anywhere in the body," and it closes the class of defect that
|
|
117
|
+
* motivated this ruling — a caller-supplied field that smuggled a raw
|
|
118
|
+
* newline followed by a `VERDICT:`- or `Judged head:`-shaped line could
|
|
119
|
+
* previously inject a structural line from outside the command's own
|
|
120
|
+
* three-line skeleton; that line can now never be read as one.
|
|
121
|
+
*/
|
|
122
|
+
function firstThreeLines(comment: string): string {
|
|
123
|
+
return comment.split('\n').slice(0, 3).join('\n')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function extractHeadSha(comment: string): string | null {
|
|
127
|
+
const m = firstThreeLines(comment).match(HEAD_SHA_PATTERN)
|
|
128
|
+
return m ? (m[1] as string).toLowerCase() : null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* `headSha` is `null` in two distinct situations that both mean "cannot
|
|
133
|
+
* confirm this verdict covers the current head": no verdict comment matched
|
|
134
|
+
* at all (`danglingNote` is also set), or a verdict comment matched but
|
|
135
|
+
* carried no `Judged head:` line (`danglingNote` is `null` — the verdict
|
|
136
|
+
* itself is real, only the binding is missing). `review-gate.ts` treats both
|
|
137
|
+
* as unbound; only their `danglingNote`/`value` differ.
|
|
138
|
+
*/
|
|
139
|
+
export type VerdictExtraction = { value: string; headSha: string | null; danglingNote: string | null }
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Round 5 (`#392`): the candidate set — which comments even ATTEMPTED a
|
|
143
|
+
* verdict — is a whole-body test, the pre-round-4 pattern. Only the VALUE is
|
|
144
|
+
* read from the first three lines (round 4, kept). Round 4's own change
|
|
145
|
+
* built `clearHits` from the windowed text directly, which made a comment
|
|
146
|
+
* whose marker sits below line 3 vanish from consideration entirely rather
|
|
147
|
+
* than counting as present-but-unclear — "most recent clear hit wins" then
|
|
148
|
+
* silently fell through to an OLDER comment, so a later blocking verdict
|
|
149
|
+
* that happened to render past line 3 let an earlier clean pass win. Here,
|
|
150
|
+
* the most recent CANDIDATE is picked first; only then is its value read
|
|
151
|
+
* from its own first three lines. A candidate whose window carries no value
|
|
152
|
+
* is DANGLING — never skipped in favour of an earlier one — so a later
|
|
153
|
+
* unclear comment still shadows an earlier clean verdict, the fail-closed
|
|
154
|
+
* direction every other branch of this function already takes.
|
|
155
|
+
*/
|
|
156
|
+
function extractVerdict(comments: string[], valuePattern: RegExp, missingLabel: string): VerdictExtraction {
|
|
157
|
+
const candidates = comments.filter((c) => valuePattern.test(c))
|
|
158
|
+
if (candidates.length === 0) {
|
|
159
|
+
return {
|
|
160
|
+
value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
|
|
161
|
+
headSha: null,
|
|
162
|
+
danglingNote: `no ${missingLabel} verdict comment found on this PR`
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const latest = candidates[candidates.length - 1] as string
|
|
167
|
+
const m = firstThreeLines(latest).match(valuePattern)
|
|
168
|
+
if (!m) {
|
|
169
|
+
return {
|
|
170
|
+
value: `the most recent ${missingLabel} comment's VERDICT line is not within its first three lines — DANGLING, see below`,
|
|
171
|
+
headSha: null,
|
|
172
|
+
danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-three-line read window`
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
value: (m[1] as string).toUpperCase().replace(/[_-]/g, ' '),
|
|
178
|
+
headSha: extractHeadSha(latest),
|
|
179
|
+
danglingNote: null
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** `value` is `APPROVE`, `REQUEST CHANGES`, `LGTM`, or a DANGLING placeholder string. */
|
|
184
|
+
export function extractCodeReviewVerdict(comments: string[]): VerdictExtraction {
|
|
185
|
+
return extractVerdict(
|
|
186
|
+
comments,
|
|
187
|
+
/^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(APPROVE|REQUEST[ _-]?CHANGES|LGTM)(?![A-Za-z0-9])/im,
|
|
188
|
+
'code-reviewer'
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** `value` is `PASS`, `FAIL`, or a DANGLING placeholder string. */
|
|
193
|
+
export function extractSecurityReviewVerdict(comments: string[]): VerdictExtraction {
|
|
194
|
+
return extractVerdict(
|
|
195
|
+
comments,
|
|
196
|
+
/^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(PASS|FAIL)(?![A-Za-z0-9])/im,
|
|
197
|
+
'security-review'
|
|
198
|
+
)
|
|
199
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vocabulary-citation.ts — a generic, regex-parameterized citation checker.
|
|
3
|
+
*
|
|
4
|
+
* `retired-vocabulary.test.ts` hand-rolls this exact sweep shape (pattern +
|
|
5
|
+
* scope + exemptions + a non-vacuity self-test) for this repo's own internal
|
|
6
|
+
* use, but as a `vitest` file no adopter can install it — and it hardcodes
|
|
7
|
+
* every pattern, scope and exemption as a repo-specific literal. This module
|
|
8
|
+
* generalizes the sweep into a reusable primitive: every pattern, its scope,
|
|
9
|
+
* its exemptions, and the sample that proves it is not vacuous arrive as
|
|
10
|
+
* caller-supplied input. It contains no product name, host-tool path, or
|
|
11
|
+
* any other repo-specific literal — that knowledge belongs to whoever
|
|
12
|
+
* configures an instance (a Vinaya check bin is one such consumer).
|
|
13
|
+
*
|
|
14
|
+
* Pure — no `fs`, no child-process I/O. The caller injects `grepFn`/
|
|
15
|
+
* `matchFn`, mirroring `evaluateC5`'s `fileExists` injection in
|
|
16
|
+
* `doc-owners.ts`: this file decides what counts as a finding, the I/O shim
|
|
17
|
+
* decides how to look.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type VocabularyPattern = {
|
|
21
|
+
/** Short, stable id for reporting — never the raw regex, which can be long. */
|
|
22
|
+
id: string
|
|
23
|
+
/**
|
|
24
|
+
* POSIX ERE, `grep -E` compatible. Never `\d`/`\w`/`\s` — GNU grep reads
|
|
25
|
+
* those as literal characters, not shorthand classes, so a pattern relying
|
|
26
|
+
* on them can pass locally (a permissive grep) and go silently vacuous on
|
|
27
|
+
* CI (GNU grep). Use `[0-9]` etc. instead. `\b` is safe: both GNU and BSD
|
|
28
|
+
* `grep -E` support it as an extension.
|
|
29
|
+
*/
|
|
30
|
+
pattern: string
|
|
31
|
+
/** Repo-root-relative paths/dirs this pattern is checked against. */
|
|
32
|
+
scope: string[]
|
|
33
|
+
/** Path substrings exempted for this pattern only, beyond `globalExempt`. */
|
|
34
|
+
exempt?: string[]
|
|
35
|
+
/**
|
|
36
|
+
* Substrings that, when present on the matched LINE, make the hit legitimate.
|
|
37
|
+
* Distinct from `exempt`, which filters by path: a name can be leakage in one
|
|
38
|
+
* sentence and the product's own identifier in the next, within the same file.
|
|
39
|
+
* An npm scope or a domain that merely CONTAINS a product name is the product
|
|
40
|
+
* naming itself, not an unexplained reference an adopter cannot resolve —
|
|
41
|
+
* exempting the whole file would blind the pattern to real leakage beside it.
|
|
42
|
+
*
|
|
43
|
+
* Granularity is the LINE, which is the accepted limit: a genuine leak that
|
|
44
|
+
* shares a line with a legitimate citation escapes. Prose puts them on
|
|
45
|
+
* separate lines in practice, and the alternative — exempting only the
|
|
46
|
+
* matched span — would need column offsets `grep -n` does not report.
|
|
47
|
+
*/
|
|
48
|
+
exemptContent?: string[]
|
|
49
|
+
/** A string this pattern MUST match — proves the pattern is not vacuous. */
|
|
50
|
+
sample: string
|
|
51
|
+
/**
|
|
52
|
+
* When true, a hit on a line that is a markdown table row (starts with
|
|
53
|
+
* `|`, ignoring leading whitespace) is not a finding. A registry table's
|
|
54
|
+
* "implementation" column citing a harness path is verified by a gate
|
|
55
|
+
* (e.g. `verify-registry.ts`'s G1/G2), not a claim stated as normative
|
|
56
|
+
* prose — banning the shape would flag the very registry a widened
|
|
57
|
+
* pattern exists to protect.
|
|
58
|
+
*/
|
|
59
|
+
exemptTableRow?: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type VocabularyHit = { file: string; line: number; content: string }
|
|
63
|
+
|
|
64
|
+
export type VocabularyFinding = VocabularyHit & { patternId: string }
|
|
65
|
+
|
|
66
|
+
export type VocabularyCheckResult = {
|
|
67
|
+
findings: VocabularyFinding[]
|
|
68
|
+
/**
|
|
69
|
+
* Patterns whose sample failed to self-match — a config bug, reported
|
|
70
|
+
* loud instead of silently passing. A pattern that matches nothing passes
|
|
71
|
+
* every scope silently, which is not hypothetical: this is the exact
|
|
72
|
+
* failure mode `retired-vocabulary.test.ts`'s own non-vacuity suite exists
|
|
73
|
+
* to catch (`\bD-(###|NNN)\b` shipped green over eleven live sites because
|
|
74
|
+
* `\b` cannot anchor after `#`).
|
|
75
|
+
*/
|
|
76
|
+
vacuousPatterns: string[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const TABLE_ROW = /^\s*\|/
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Evaluates every pattern against its own scope, filtering exempted paths
|
|
83
|
+
* and (optionally) table-row citations, and reports which patterns proved
|
|
84
|
+
* vacuous against their own sample instead of silently returning clean.
|
|
85
|
+
*/
|
|
86
|
+
export function evaluateVocabularyCitation(
|
|
87
|
+
patterns: VocabularyPattern[],
|
|
88
|
+
grepFn: (pattern: string, scope: string[]) => VocabularyHit[],
|
|
89
|
+
matchFn: (pattern: string, sample: string) => boolean,
|
|
90
|
+
globalExempt: string[] = []
|
|
91
|
+
): VocabularyCheckResult {
|
|
92
|
+
const findings: VocabularyFinding[] = []
|
|
93
|
+
const vacuousPatterns: string[] = []
|
|
94
|
+
|
|
95
|
+
for (const p of patterns) {
|
|
96
|
+
if (!matchFn(p.pattern, p.sample)) {
|
|
97
|
+
vacuousPatterns.push(p.id)
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const exempt = [...globalExempt, ...(p.exempt ?? [])]
|
|
102
|
+
const exemptContent = p.exemptContent ?? []
|
|
103
|
+
const hits = grepFn(p.pattern, p.scope).filter((h) => {
|
|
104
|
+
if (exempt.some((e) => h.file.includes(e))) return false
|
|
105
|
+
if (exemptContent.some((e) => h.content.includes(e))) return false
|
|
106
|
+
if (p.exemptTableRow && TABLE_ROW.test(h.content)) return false
|
|
107
|
+
return true
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
for (const h of hits) findings.push({ ...h, patternId: p.id })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { findings, vacuousPatterns }
|
|
114
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waiver-label actor verification. A doc-coverage waiver is honored
|
|
3
|
+
* ONLY when the `vinaya/waiver:docs` PR label is present AND the actor of that
|
|
4
|
+
* label's most recent labeling timeline event is a configured principal.
|
|
5
|
+
* Label presence alone is never sufficient — that is exactly the
|
|
6
|
+
* agent-emittable-string hole closes (supersedes's
|
|
7
|
+
* `Doc-waiver:` grammar). Pure — no env reads here; I/O stays in the CLI
|
|
8
|
+
* shim, same discipline as `pr-tier.ts`.
|
|
9
|
+
*
|
|
10
|
+
* `isWaiverLabelActorVerified` is parameterized by `label` (aeg-review-gate-v1
|
|
11
|
+
* task 1, #474) so a second waiver label (`vinaya/waiver:review`) can reuse the exact
|
|
12
|
+
* same actor-verification predicate rather than a copy-pasted duplicate that
|
|
13
|
+
* could drift out of sync.
|
|
14
|
+
*
|
|
15
|
+
* The label strings themselves are read from the code-owned vocabulary in
|
|
16
|
+
* `@attalabs/aeg-forge-state`'s `labels.ts`, never written as literals here — a
|
|
17
|
+
* namespace change lands in one file (discipline).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { label as labelFor } from '@attalabs/aeg-forge-state'
|
|
21
|
+
|
|
22
|
+
export const WAIVER_LABEL = labelFor('waiver-docs')
|
|
23
|
+
export const WAIVER_LABEL_REVIEW = labelFor('waiver-review')
|
|
24
|
+
export const PRINCIPAL_ALLOWLIST = ['daniboomerang']
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Membership test for a principal allowlist. **Case-insensitive, because
|
|
28
|
+
* GitHub logins are** — `Alice` and `alice` address the same account, and the
|
|
29
|
+
* forge may return either casing depending on the API surface. An exact-match
|
|
30
|
+
* test fails closed, but it fails closed *invisibly*: an adopter who wrote
|
|
31
|
+
* `"principals": ["Alice"]` would watch Alice's genuine verdicts land as
|
|
32
|
+
* DANGLING with no stated reason — precisely the baffling symptom the
|
|
33
|
+
* `principals` field exists to remove (review finding, PR #862).
|
|
34
|
+
*
|
|
35
|
+
* Shared by every allowlist consumer so the waiver check and the review gate
|
|
36
|
+
* can never disagree about whether a given login is a principal.
|
|
37
|
+
*/
|
|
38
|
+
export function isPrincipal(login: string | null, principalAllowlist: string[]): boolean {
|
|
39
|
+
if (login === null) return false
|
|
40
|
+
const normalized = login.toLowerCase()
|
|
41
|
+
return principalAllowlist.some((p) => p.toLowerCase() === normalized)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isWaiverLabelActorVerified(opts: {
|
|
45
|
+
label: string
|
|
46
|
+
labels: string[]
|
|
47
|
+
labelActor: string | null
|
|
48
|
+
principalAllowlist: string[]
|
|
49
|
+
}): boolean {
|
|
50
|
+
if (!opts.labels.includes(opts.label)) return false
|
|
51
|
+
return isPrincipal(opts.labelActor, opts.principalAllowlist)
|
|
52
|
+
}
|