@attalabs/vinaya 0.21.0 → 0.23.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 +15 -4
- 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 +28 -21
- 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 +2 -2
- 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 +12 -7
- package/aeg-root/tranche-model.md +9 -5
- package/dist/checks/bin/check-body-bare-digits.js +667 -148
- package/dist/checks/bin/check-branch-topology.js +714 -149
- package/dist/checks/bin/check-brief-shape.js +704 -147
- package/dist/checks/bin/check-changeset-coverage.js +1046 -166
- package/dist/checks/bin/check-closes-n.js +714 -149
- package/dist/checks/bin/check-coherence.js +729 -156
- package/dist/checks/bin/check-dead-branch-push.js +663 -144
- package/dist/checks/bin/check-dispatch-readiness.js +728 -155
- package/dist/checks/bin/check-doc-coverage-push.js +838 -261
- package/dist/checks/bin/check-doc-coverage.js +838 -261
- package/dist/checks/bin/check-doctrine-no-procedures.js +4794 -0
- package/dist/checks/bin/check-doctrine-portability.js +799 -155
- package/dist/checks/bin/check-evidence-fresh.js +1132 -206
- package/dist/checks/bin/check-exec-bits.js +4931 -0
- package/dist/checks/bin/check-first-push-dispatch.js +716 -151
- package/dist/checks/bin/check-issue-assignment.js +714 -149
- package/dist/checks/bin/check-main-branch-refusal.js +663 -144
- package/dist/checks/bin/check-no-disk-state.js +663 -144
- package/dist/checks/bin/check-pr-report-density.js +4461 -0
- package/dist/checks/bin/check-quoted-command.js +741 -166
- package/dist/checks/bin/check-reader-resolvable-prose.js +768 -173
- package/dist/checks/bin/check-registry-gates.js +726 -154
- package/dist/checks/bin/check-retired-vocabulary.js +760 -165
- package/dist/checks/bin/check-review-gate.js +783 -148
- package/dist/checks/bin/check-single-plan-pr.js +663 -144
- package/dist/checks/bin/check-test-plan.js +664 -145
- package/dist/checks/bin/check-token-collection-wired.js +665 -146
- package/dist/checks/bin/check-token-report.js +4509 -0
- package/dist/checks/bin/check-workspace-escape.js +1129 -183
- package/dist/index.js +8458 -5403
- 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 +113 -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 +192 -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-validation.ts +511 -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 +787 -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 +224 -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-portability.ts +170 -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 +168 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/index.ts +301 -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 +165 -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-tier.ts +81 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/premise-check.ts +125 -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 +269 -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 +104 -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 +140 -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]__02c71kv._.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]__0ozbaq0._.js → [root-of-the-server]__1mcitst._.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/_0o54suy._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1-6rzip._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1mrtj0f._.js +1 -1
- 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_06it6s-._.js +1 -1
- 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/chunks/ssr/packages_ui_libraries_animate_components_index_ts_186fje1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_brutal_components_index_ts_0n_lcc4._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_retro_components_index_ts_0wvpm_z._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_retro_components_index_ts_1_cfi48._.js +1 -1
- 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/00pi8vhg8w6gi.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0r4i_ks9z02bw.js → 03q0q9n6-xah8.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2_k6j1o9q5nn4.js → 0jer_drkl1e4r.js} +4 -4
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0vc2u252q5q6o.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1k916ih3p4nsg.js → 13g16cu00g570.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2hmannd4bjhzy.js → 1sfxfunirkcr6.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1t2kft7bxzuxn.js → 206ajg8f1a-_h.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1hfhxjy577l5d.js → 20x-t0a0v129z.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{25kbt5k-xb2a5.js → 279qq4twinc97.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{112g08xpx3kro.js → 2ka8dg687auhj.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/2ou_81ahqw31e.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0up5x3jg-cwov.js → 2rn89uwtf2lwc.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3mqu_uhluy19i.css → 3qc67qhcvbg0o.css} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1yqs2zgvwa4p1.js → 3zugx9esh2x51.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0_jnwbarv-cqk.js +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1ybrihl5_aocy.js +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3r5efmleneaco.js +0 -1
- /package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{crNCHQ9uAgKOYC6DKkZma → 5xy_GA7G5KRuE3HDumIiw}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{crNCHQ9uAgKOYC6DKkZma → 5xy_GA7G5KRuE3HDumIiw}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{crNCHQ9uAgKOYC6DKkZma → 5xy_GA7G5KRuE3HDumIiw}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Milestone-shape validation for `vinaya milestone create` (vinaya-milestone-model-v1
|
|
3
|
+
* task 2). Pure — no `fs`, no `gh`. The command calls `checkMilestoneShape` before
|
|
4
|
+
* any forge write, so a malformed body never reaches GitHub.
|
|
5
|
+
*
|
|
6
|
+
* A Milestone's description carries three independent things, each parsed and
|
|
7
|
+
* removed from the remainder in turn: an optional `Release:` field (the sole
|
|
8
|
+
* authority for the milestone's version — never the title), an optional
|
|
9
|
+
* `### Tranche intents` section (one bullet per tranche this milestone will
|
|
10
|
+
* eventually hold, `- <slug>: <intent text>`), and the goal (whatever prose is
|
|
11
|
+
* left once both are removed). The goal is mandatory; `Release:` and the
|
|
12
|
+
* intents section are each optional but must parse if present.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { stripCode } from '@attalabs/aeg-forge-state'
|
|
16
|
+
|
|
17
|
+
export type MilestoneIntent = { slug: string; goal: string }
|
|
18
|
+
|
|
19
|
+
export type ReleaseField = {
|
|
20
|
+
/** A `Release:` field line exists outside code. Says nothing about whether its value parsed. */
|
|
21
|
+
declared: boolean
|
|
22
|
+
/** The parsed version string, or `null` when absent or malformed. */
|
|
23
|
+
value: string | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type MilestoneShapeResult =
|
|
27
|
+
| { status: 'pass'; goal: string; release: string | null; intents: MilestoneIntent[] }
|
|
28
|
+
| { status: 'fail'; errors: string[] }
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Line-anchored, `**`-optional on both sides, same shape as `PROJECT_FIELD`
|
|
32
|
+
* (`@attalabs/aeg-forge-state`'s `list-tasks.ts`) — the reference grammar this
|
|
33
|
+
* field is deliberately matched against rather than approximated. Un-global
|
|
34
|
+
* and `.exec`'d once: first match wins.
|
|
35
|
+
*/
|
|
36
|
+
const RELEASE_FIELD = /^\s*(?:\*\*)?Release(?:\*\*)?\s*:\s*(?:\*\*)?\s*(.+)$/im
|
|
37
|
+
|
|
38
|
+
/** Peels the markup/punctuation a field value carries in prose — same edge set as `unwrapValue`. */
|
|
39
|
+
const VALUE_EDGE_MARKUP = /^[`.;\s]+|[`.;\s]+$/g
|
|
40
|
+
|
|
41
|
+
function unwrapValue(raw: string): string {
|
|
42
|
+
return raw.replace(/\*\*/g, '').replace(VALUE_EDGE_MARKUP, '')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A semver-shaped version: optional `v` prefix, `MAJOR.MINOR.PATCH`, optional pre-release/build. */
|
|
46
|
+
const RELEASE_VALUE = /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?$/
|
|
47
|
+
|
|
48
|
+
/** Text with code fences/indented blocks stripped, inline spans kept — same reading `PROJECT_FIELD` uses. */
|
|
49
|
+
function pathText(body: string): string {
|
|
50
|
+
return stripCode(body, { inlineSpans: 'keep' })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The actual reader, over already-stripped text — shared so `checkMilestoneShape` (which already holds `text`) never pays for a second `stripCode` pass over the same body. */
|
|
54
|
+
function releaseFieldFromText(text: string): ReleaseField {
|
|
55
|
+
const m = text.match(RELEASE_FIELD)
|
|
56
|
+
if (!m) return { declared: false, value: null }
|
|
57
|
+
const raw = unwrapValue(m[1] ?? '')
|
|
58
|
+
return { declared: true, value: RELEASE_VALUE.test(raw) ? raw : null }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Reads the `Release:` field. `declared: true, value: null` is the malformed
|
|
63
|
+
* case — the field exists but its value isn't a version — distinct from
|
|
64
|
+
* `declared: false` (no field at all, which is a valid, versionless milestone).
|
|
65
|
+
*/
|
|
66
|
+
export function releaseFieldFromBody(body: string): ReleaseField {
|
|
67
|
+
return releaseFieldFromText(pathText(body))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im
|
|
71
|
+
const NEXT_HEADING = /^#{1,6}\s+\S/m
|
|
72
|
+
const INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i
|
|
73
|
+
|
|
74
|
+
/** The intents heading's own span plus its section body — `null` when there is no heading at all. */
|
|
75
|
+
function intentsBlock(text: string): { start: number; end: number; section: string } | null {
|
|
76
|
+
const start = text.match(INTENTS_HEADING)
|
|
77
|
+
if (!start || start.index === undefined) return null
|
|
78
|
+
const rest = text.slice(start.index + start[0].length)
|
|
79
|
+
const next = rest.match(NEXT_HEADING)
|
|
80
|
+
const sectionEnd = next && next.index !== undefined ? next.index : rest.length
|
|
81
|
+
return { start: start.index, end: start.index + start[0].length + sectionEnd, section: rest.slice(0, sectionEnd) }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Slices the intents section out of `pathText(body)` — from just after the heading to the next heading or end. */
|
|
85
|
+
function intentsSectionText(text: string): string | null {
|
|
86
|
+
return intentsBlock(text)?.section ?? null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Parses the `### Tranche intents` section, if present. `null` means no
|
|
91
|
+
* heading at all (a valid, intent-less milestone). A heading whose non-blank
|
|
92
|
+
* lines don't all match the bullet grammar is malformed — reported via
|
|
93
|
+
* `malformed: true` rather than silently dropping the bad line, mirroring
|
|
94
|
+
* `ProjectField.unparsed`'s fail-closed discipline.
|
|
95
|
+
*/
|
|
96
|
+
function parseIntents(text: string): { intents: MilestoneIntent[]; malformed: boolean } {
|
|
97
|
+
const section = intentsSectionText(text)
|
|
98
|
+
if (section === null) return { intents: [], malformed: false }
|
|
99
|
+
|
|
100
|
+
const intents: MilestoneIntent[] = []
|
|
101
|
+
for (const line of section.split('\n')) {
|
|
102
|
+
const trimmed = line.trim()
|
|
103
|
+
if (trimmed.length === 0) continue
|
|
104
|
+
const m = trimmed.match(INTENT_BULLET)
|
|
105
|
+
if (!m) return { intents: [], malformed: true }
|
|
106
|
+
intents.push({ slug: (m[1] ?? '').toLowerCase(), goal: (m[2] ?? '').trim() })
|
|
107
|
+
}
|
|
108
|
+
return { intents, malformed: false }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Removes the first `Release:` line and the whole intents section (heading included) from `pathText(body)`. */
|
|
112
|
+
function goalRemainder(text: string): string {
|
|
113
|
+
const withoutRelease = text.replace(RELEASE_FIELD, '')
|
|
114
|
+
const block = intentsBlock(withoutRelease)
|
|
115
|
+
const remainder = block ? withoutRelease.slice(0, block.start) + withoutRelease.slice(block.end) : withoutRelease
|
|
116
|
+
return remainder.trim()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Refuses a milestone body before any forge write: the goal absent, `Release:`
|
|
121
|
+
* present but malformed, or the intents section unparseable. Shape-guarded
|
|
122
|
+
* only — like `checkIssueRationale`, this checks presence/well-formedness,
|
|
123
|
+
* never whether the content is right.
|
|
124
|
+
*/
|
|
125
|
+
export function checkMilestoneShape(body: string): MilestoneShapeResult {
|
|
126
|
+
const errors: string[] = []
|
|
127
|
+
const text = pathText(body)
|
|
128
|
+
|
|
129
|
+
const release = releaseFieldFromText(text)
|
|
130
|
+
if (release.declared && release.value === null) {
|
|
131
|
+
errors.push(
|
|
132
|
+
'milestone-validation Release: the `Release:` field is present but is not a version — ' +
|
|
133
|
+
'write a semver value (e.g. `Release: 1.2.0`), or omit the field entirely for a milestone that declares no version.'
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const { intents, malformed } = parseIntents(text)
|
|
138
|
+
if (malformed) {
|
|
139
|
+
errors.push(
|
|
140
|
+
'milestone-validation intents: the `### Tranche intents` section does not parse — every non-blank ' +
|
|
141
|
+
'line under it must read `- <slug>: <intent text>`.'
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const goal = goalRemainder(text)
|
|
146
|
+
if (goal.length === 0) {
|
|
147
|
+
errors.push(
|
|
148
|
+
'milestone-validation goal: no goal text — a milestone description must carry prose beyond its ' +
|
|
149
|
+
'`Release:` field and `### Tranche intents` section.'
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (errors.length > 0) return { status: 'fail', errors }
|
|
154
|
+
return { status: 'pass', goal, release: release.value, intents }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// `vinaya milestone adopt` (vinaya-milestone-migration-v1 task 1) — checkAdoptable
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* One requested slug's forge facts, gathered by the CLI before any write.
|
|
163
|
+
* `checkAdoptable` never fetches — the caller does one bounded round of
|
|
164
|
+
* reads (the repo's label list, the repo's Milestone list, and one
|
|
165
|
+
* `vinaya/tranche:<slug>`-labeled Issue list per requested slug) and hands
|
|
166
|
+
* the results in as plain data.
|
|
167
|
+
*/
|
|
168
|
+
export type AdoptSlugFacts = {
|
|
169
|
+
slug: string
|
|
170
|
+
/** Whether `vinaya/tranche:<slug>` exists as a real label in this repo. */
|
|
171
|
+
labelExists: boolean
|
|
172
|
+
/** Issue numbers currently carrying the label, any state. */
|
|
173
|
+
issueNumbers: number[]
|
|
174
|
+
/**
|
|
175
|
+
* Native-milestone titles this slug's Issues are CURRENTLY attached to,
|
|
176
|
+
* excluding `null` (unattached — the ordinary state for a label-only
|
|
177
|
+
* tranche that has never been adopted), the slug itself (its own legacy
|
|
178
|
+
* 1:1 tranche-Milestone — the ordinary pre-adopt state for a
|
|
179
|
+
* legacy-titled tranche), and the requested target (already adopted here,
|
|
180
|
+
* a harmless no-op re-run). What remains is exactly "attached to some
|
|
181
|
+
* OTHER Milestone" — evidence of a prior `adopt` into a different target.
|
|
182
|
+
* Empty when none.
|
|
183
|
+
*/
|
|
184
|
+
adoptedElsewhere: string[]
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The target Milestone's forge facts. */
|
|
188
|
+
export type AdoptTargetFacts = {
|
|
189
|
+
title: string
|
|
190
|
+
exists: boolean
|
|
191
|
+
state: 'open' | 'closed' | null
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export type AdoptFacts = {
|
|
195
|
+
target: AdoptTargetFacts
|
|
196
|
+
slugs: AdoptSlugFacts[]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export type AdoptResult = { status: 'pass' } | { status: 'fail'; errors: string[] }
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Refuses an `adopt` invocation before any forge write. Four independent
|
|
203
|
+
* checks — an unknown slug, a slug whose label carries no Issues, a target
|
|
204
|
+
* that does not exist or is closed, and a slug already adopted into a
|
|
205
|
+
* different Milestone — evaluated over the WHOLE batch of requested slugs at
|
|
206
|
+
* once, so one bad slug in a multi-slug invocation blocks every slug in that
|
|
207
|
+
* invocation, not just its own: a half-applied adopt leaves Issues split
|
|
208
|
+
* across two Milestones with no undo. Pure: takes already-gathered facts,
|
|
209
|
+
* fetches nothing, writes nothing.
|
|
210
|
+
*/
|
|
211
|
+
export function checkAdoptable(facts: AdoptFacts): AdoptResult {
|
|
212
|
+
const errors: string[] = []
|
|
213
|
+
|
|
214
|
+
if (!facts.target.exists) {
|
|
215
|
+
errors.push(
|
|
216
|
+
`milestone-adopt target: Milestone "${facts.target.title}" does not exist — create it first with \`vinaya milestone create\`.`
|
|
217
|
+
)
|
|
218
|
+
} else if (facts.target.state === 'closed') {
|
|
219
|
+
errors.push(`milestone-adopt target: Milestone "${facts.target.title}" is closed — adopt requires an open target.`)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for (const s of facts.slugs) {
|
|
223
|
+
if (!s.labelExists) {
|
|
224
|
+
errors.push(`milestone-adopt unknown-slug: no \`vinaya/tranche:${s.slug}\` label exists in this repo.`)
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
if (s.issueNumbers.length === 0) {
|
|
228
|
+
errors.push(`milestone-adopt no-issues: \`vinaya/tranche:${s.slug}\` carries no Issues — nothing to adopt.`)
|
|
229
|
+
continue
|
|
230
|
+
}
|
|
231
|
+
if (s.adoptedElsewhere.length > 0) {
|
|
232
|
+
errors.push(
|
|
233
|
+
`milestone-adopt already-adopted: "${s.slug}" is already adopted into "${s.adoptedElsewhere.join('", "')}" — adopt it from there, not from here.`
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (errors.length > 0) return { status: 'fail', errors }
|
|
239
|
+
return { status: 'pass' }
|
|
240
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* New-on-disk-state guard (#512 Part D). Pure — no `fs`, no `git`/`gh` I/O.
|
|
3
|
+
* Flags a diff-touched path as a disk-state offender given its change
|
|
4
|
+
* status (`added` — the path didn't exist on the base ref — or `modified`
|
|
5
|
+
* — it already did):
|
|
6
|
+
*
|
|
7
|
+
* (a) A live tranche topology file directly under `aeg-root/tranches/`
|
|
8
|
+
* (not a subdirectory) — the residue class this task's own Part B proved
|
|
9
|
+
* unnecessary (forge derivation already covers it, see
|
|
10
|
+
* `aeg-drift-prevention-v1.md`'s deletion). Fails on `added` OR
|
|
11
|
+
* `modified`: this file class shouldn't exist at all post-cutover,
|
|
12
|
+
* whether newly created or resurrected via an edit.
|
|
13
|
+
*
|
|
14
|
+
* (b) Any `.md` file anywhere under `aeg-root/tranches/` (any depth,
|
|
15
|
+
* including `completed/**`) — but ONLY when
|
|
16
|
+
* `added`. `completed/**`'s existing files are legacy archive
|
|
17
|
+
* (explicitly excludes them, per `tranche-model.md` §4/§11 —
|
|
18
|
+
* they're never deleted or migrated, and editing one to fix a typo
|
|
19
|
+
* must stay legal); this rule instead closes the gap a path-shape-only
|
|
20
|
+
* exemption would leave open — a BRAND NEW file smuggled directly into
|
|
21
|
+
* `completed/` (or any other subdirectory) to dodge rule (a)'s
|
|
22
|
+
* top-level check. Deliberately status-based, not a hardcoded filename
|
|
23
|
+
* allowlist: the legacy set only grows via the (currently-dormant,
|
|
24
|
+
* forge-native-cutover-pending) Archivist move-to-completed flow, never
|
|
25
|
+
* via this gate's own exemption logic.
|
|
26
|
+
*
|
|
27
|
+
* (c) Any `*.tokens.md` file, anywhere in the repo — the pre- ledger
|
|
28
|
+
* shape `Tokens-in-PR-body` superseded — but ONLY when `added` (an
|
|
29
|
+
* edit to one of the 4 existing legacy `completed/*.tokens.md` files
|
|
30
|
+
* must stay legal, same reasoning as (b)).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export type DiskStateFileStatus = 'added' | 'modified'
|
|
34
|
+
|
|
35
|
+
const TOP_LEVEL_TOPOLOGY_FILE = /^aeg-root\/tranches\/[^/]+\.md$/
|
|
36
|
+
const ANY_DEPTH_TRANCHES_MD = /^aeg-root\/tranches\/.*\.md$/
|
|
37
|
+
const TOKENS_FILE = /\.tokens\.md$/
|
|
38
|
+
|
|
39
|
+
export function isNewDiskStateFile(path: string, status: DiskStateFileStatus): boolean {
|
|
40
|
+
// The tranche model doc used to live at `aeg-root/tranches/README.md`
|
|
41
|
+
// and needed an explicit carve-out here so editing it never read as new disk
|
|
42
|
+
// state. It is now `aeg-root/tranche-model.md`, outside the directory this
|
|
43
|
+
// guard watches, so the carve-out is gone rather than left as a line that
|
|
44
|
+
// can never fire.
|
|
45
|
+
if (TOP_LEVEL_TOPOLOGY_FILE.test(path)) return true
|
|
46
|
+
|
|
47
|
+
if (status !== 'added') return false
|
|
48
|
+
|
|
49
|
+
return ANY_DEPTH_TRANCHES_MD.test(path) || TOKENS_FILE.test(path)
|
|
50
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readMarkdownTable } from './parse-registry'
|
|
2
|
+
import type { LedgerRow } from './types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse the append-only token/cost ledger for a tranche. See
|
|
6
|
+
* `aeg-root/tranche-model.md` §12 for the canonical format.
|
|
7
|
+
*
|
|
8
|
+
* The ledger lives in a sibling file `aeg-root/tranches/<name>.tokens.md`
|
|
9
|
+
* (the recommended home, since two roles appending rows do not collide with
|
|
10
|
+
* a Planner editing the topology file) but the parser also accepts a
|
|
11
|
+
* `## Token ledger` section inside any markdown — so a future tranche that
|
|
12
|
+
* chose the inline form still parses.
|
|
13
|
+
*
|
|
14
|
+
* Pure: no I/O. The caller reads the file contents and hands them in.
|
|
15
|
+
*
|
|
16
|
+
* Tolerant by design: a malformed row is skipped rather than throwing, so a
|
|
17
|
+
* typo does not crash Studio's display of a tranche's other rows.
|
|
18
|
+
*/
|
|
19
|
+
export function parseLedger(md: string): LedgerRow[] {
|
|
20
|
+
const rows = readMarkdownTable(md, /^#{1,6}\s+Token\s+ledger\b/i)
|
|
21
|
+
const out: LedgerRow[] = []
|
|
22
|
+
for (const row of rows) {
|
|
23
|
+
const parsed = rowFromCells(row)
|
|
24
|
+
if (parsed) out.push(parsed)
|
|
25
|
+
}
|
|
26
|
+
return out
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Turn one 7-cell row (`Phase | Role | Agent/Model | Tokens in | Tokens out |
|
|
31
|
+
* Cost | Date`) into a `LedgerRow`, or `null` when the row is unusable.
|
|
32
|
+
* Exported so other sources that produce the same row-shape from a different
|
|
33
|
+
* layout (e.g. `parse-token-report.ts`'s PR-body/verdict-comment sources)
|
|
34
|
+
* reuse this exact cell-parsing/null-tolerance semantics rather than
|
|
35
|
+
* re-implementing it — see that file's module docstring.
|
|
36
|
+
*/
|
|
37
|
+
export function rowFromCells(cells: string[]): LedgerRow | null {
|
|
38
|
+
// Expected 7 columns: Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date.
|
|
39
|
+
if (cells.length < 7) return null
|
|
40
|
+
const phase = (cells[0] ?? '').trim()
|
|
41
|
+
// Phase is the row's identity — every other cell may be unknown, but a
|
|
42
|
+
// row without a phase is unusable (it can't be located on a re-pivot).
|
|
43
|
+
if (!phase) return null
|
|
44
|
+
return {
|
|
45
|
+
phase,
|
|
46
|
+
role: (cells[1] ?? '').trim(),
|
|
47
|
+
agentModel: (cells[2] ?? '').trim(),
|
|
48
|
+
tokensIn: parseIntCell(cells[3] ?? ''),
|
|
49
|
+
tokensOut: parseIntCell(cells[4] ?? ''),
|
|
50
|
+
cost: parseCostCell(cells[5] ?? ''),
|
|
51
|
+
date: (cells[6] ?? '').trim()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseIntCell(cell: string): number | null {
|
|
56
|
+
const trimmed = cell.replace(/`/g, '').trim()
|
|
57
|
+
if (!trimmed || isEmDashOrDash(trimmed)) return null
|
|
58
|
+
// Tolerate thousand-separator commas and underscores so a hand-written
|
|
59
|
+
// `184,327` or `184_327` still resolves.
|
|
60
|
+
const normalized = trimmed.replace(/[,_\s]/g, '')
|
|
61
|
+
if (!/^-?\d+$/.test(normalized)) return null
|
|
62
|
+
return Number(normalized)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseCostCell(cell: string): number | null {
|
|
66
|
+
const trimmed = cell.replace(/`/g, '').trim()
|
|
67
|
+
if (!trimmed || isEmDashOrDash(trimmed)) return null
|
|
68
|
+
// Strip a leading `$` and any thousand-separators; preserve the decimal.
|
|
69
|
+
const normalized = trimmed.replace(/^\$/, '').replace(/[,_\s]/g, '')
|
|
70
|
+
if (!/^-?\d+(\.\d+)?$/.test(normalized)) return null
|
|
71
|
+
return Number(normalized)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isEmDashOrDash(s: string): boolean {
|
|
75
|
+
const t = s.trim()
|
|
76
|
+
return t === '—' || t === '-' || t === '–'
|
|
77
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Project, Registry } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse `aeg-root/projects.md` and return its `## Registry` table as a typed
|
|
5
|
+
* `Registry`. Surrounding prose is ignored.
|
|
6
|
+
*
|
|
7
|
+
* Tolerates a prose last-cell (e.g. "(state tracked globally for now)") by
|
|
8
|
+
* setting `statePath: null`. A real path is detected by the backtick wrapping
|
|
9
|
+
* (`<path>`) used in the live file's columns 2 + 3 + (when present) 4.
|
|
10
|
+
*
|
|
11
|
+
* The function is forgiving: a malformed row is skipped rather than throwing,
|
|
12
|
+
* so a typo in the markdown does not crash all of Studio. Validation of
|
|
13
|
+
* `Project:` references against the result is a separate concern.
|
|
14
|
+
*/
|
|
15
|
+
export function parseRegistry(md: string): Registry {
|
|
16
|
+
const rows = readMarkdownTable(md, /^##\s+Registry\b/i)
|
|
17
|
+
const projects: Project[] = []
|
|
18
|
+
for (const row of rows) {
|
|
19
|
+
// Expect 4 columns: Project | Path | Specs | Per-project state.
|
|
20
|
+
if (row.length < 4) continue
|
|
21
|
+
const name = stripBackticks(row[0] ?? '').trim()
|
|
22
|
+
const path = stripBackticks(row[1] ?? '').trim()
|
|
23
|
+
const specsPath = stripBackticks(row[2] ?? '').trim()
|
|
24
|
+
const stateCell = (row[3] ?? '').trim()
|
|
25
|
+
if (!name || !path || !specsPath) continue
|
|
26
|
+
projects.push({
|
|
27
|
+
name,
|
|
28
|
+
path,
|
|
29
|
+
specsPath,
|
|
30
|
+
statePath: parseStateCell(stateCell)
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
return projects
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseStateCell(cell: string): string | null {
|
|
37
|
+
if (!cell) return null
|
|
38
|
+
// Prose state cell — anything that doesn't look like a backtick-wrapped path.
|
|
39
|
+
// The live file uses `apps/<x>/aeg-project/` for tracked state and
|
|
40
|
+
// parenthesized prose like "(state tracked globally for now)" otherwise.
|
|
41
|
+
const stripped = stripBackticks(cell).trim()
|
|
42
|
+
// If we stripped backticks and got a path-shaped string, return it.
|
|
43
|
+
if (/^[\w./@-]+\/?$/.test(stripped) && cell.includes('`')) {
|
|
44
|
+
return stripped
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function stripBackticks(s: string): string {
|
|
50
|
+
return s.replace(/`/g, '')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generic markdown-table reader: finds the first `| … |` table block following
|
|
55
|
+
* the `headingMatcher` regex, skips the header + separator rows, and returns
|
|
56
|
+
* each body row as an array of trimmed cell strings.
|
|
57
|
+
*
|
|
58
|
+
* Pipes can be escaped inline as `\|`; this is unescaped after the split.
|
|
59
|
+
* Leading/trailing pipes are tolerated. Lines outside the table block end
|
|
60
|
+
* iteration.
|
|
61
|
+
*/
|
|
62
|
+
export function readMarkdownTable(md: string, headingMatcher: RegExp): string[][] {
|
|
63
|
+
const lines = md.split(/\r?\n/)
|
|
64
|
+
let i = 0
|
|
65
|
+
// Advance to the section heading.
|
|
66
|
+
for (; i < lines.length; i++) {
|
|
67
|
+
if (headingMatcher.test(lines[i] ?? '')) {
|
|
68
|
+
i++
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (i >= lines.length) return []
|
|
73
|
+
// Advance to the first table line.
|
|
74
|
+
for (; i < lines.length; i++) {
|
|
75
|
+
if ((lines[i] ?? '').trim().startsWith('|')) break
|
|
76
|
+
}
|
|
77
|
+
// Skip header + separator.
|
|
78
|
+
if (i + 1 >= lines.length) return []
|
|
79
|
+
i += 2
|
|
80
|
+
const out: string[][] = []
|
|
81
|
+
for (; i < lines.length; i++) {
|
|
82
|
+
const trimmed = (lines[i] ?? '').trim()
|
|
83
|
+
if (!trimmed.startsWith('|')) break
|
|
84
|
+
out.push(splitTableRow(trimmed))
|
|
85
|
+
}
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Exported for reuse by parsers that read a table body outside `readMarkdownTable`'s single-section-per-call model (e.g. `parse-token-report.ts`'s multi-entry "Token report" sections). */
|
|
90
|
+
export function splitTableRow(row: string): string[] {
|
|
91
|
+
// Use a placeholder for escaped pipes, split, then restore.
|
|
92
|
+
// The sentinel is a NUL, escaped rather than typed literally. A raw NUL byte
|
|
93
|
+
// in the source makes git classify this whole FILE as binary: `git diff`
|
|
94
|
+
// reports `Bin 3715 -> 3716 bytes, 0 insertions(+), 0 deletions(-)` instead
|
|
95
|
+
// of a reviewable diff, and `git grep -n` answers `Binary file ... matches`
|
|
96
|
+
// with no line number, so every sweep that greps this repo silently skipped
|
|
97
|
+
// it. `\u0000` is the same byte at runtime and keeps the file text.
|
|
98
|
+
const PLACEHOLDER = '\u0000PIPE\u0000'
|
|
99
|
+
const escaped = row.replace(/\\\|/g, PLACEHOLDER)
|
|
100
|
+
const parts = escaped.split('|').map((s) => s.trim())
|
|
101
|
+
// Strip the empty cells produced by leading and trailing `|`.
|
|
102
|
+
if (parts.length && parts[0] === '') parts.shift()
|
|
103
|
+
if (parts.length && parts[parts.length - 1] === '') parts.pop()
|
|
104
|
+
return parts.map((p) => p.split(PLACEHOLDER).join('|'))
|
|
105
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { rowFromCells } from './parse-ledger'
|
|
2
|
+
import { splitTableRow } from './parse-registry'
|
|
3
|
+
import type { LedgerRow } from './types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Live-source token parsing (aeg-forge-state-v1 task 4b, #445). Where
|
|
7
|
+
* `parse-ledger.ts` reads the Archivist's hand-assembled `<name>.tokens.md`
|
|
8
|
+
* ledger, this file extracts the same `LedgerRow` shape directly from the
|
|
9
|
+
* artifacts says every role already produces on its own turn — the
|
|
10
|
+
* Developer's "Token report" section in a PR body, and the Reviewer's /
|
|
11
|
+
* Security's / Planner's one-line `Tokens: …` report in a verdict comment,
|
|
12
|
+
* PR body, or planning report. Nothing here does I/O — the caller (Studio's
|
|
13
|
+
* forge adapter, `apps/vinaya/web/src/lib/forge/fetch-token-ledger.ts`)
|
|
14
|
+
* fetches the PR bodies/comments and hands them to `aggregateTaskTokenRows`.
|
|
15
|
+
*
|
|
16
|
+
* Cell parsing (`—`/null tolerance, thousand-separator commas, `$` cost
|
|
17
|
+
* prefix) is NOT reimplemented here — every row is built through
|
|
18
|
+
* `parse-ledger.ts`'s `rowFromCells`, so a live-fetched row and a
|
|
19
|
+
* `.tokens.md`-parsed row for the same underlying report are byte-identical
|
|
20
|
+
* in how their cells resolve.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// A "Token report" heading, alone on its line: `## Token report`,
|
|
24
|
+
// `### Token report`, or the bold-inline `**Token report**` form seen in the
|
|
25
|
+
// wild (real PRs use both — #412 uses `## Token report`, #454 uses the bold
|
|
26
|
+
// form). A PR re-pushed after `CHANGES_REQUESTED` carries this heading more
|
|
27
|
+
// than once — every occurrence is parsed as its own entry (roles/developer.md:
|
|
28
|
+
// re-entry appends a new "Token report" entry, never edits the first).
|
|
29
|
+
const TOKEN_REPORT_HEADING = /^\s*(?:#{1,6}\s*Token report\s*|\*\*Token report\*\*)\s*$/i
|
|
30
|
+
|
|
31
|
+
// Inline field-list form, a real drift already present in this repo's own
|
|
32
|
+
// history (PR #374, #362) alongside the table form: one line reading
|
|
33
|
+
// `Phase: … | Role: … | Agent/Model: … | Tokens in: … | Tokens out: … |
|
|
34
|
+
// Cost: … | Date: …`. Tolerated as a second recognized shape rather than
|
|
35
|
+
// treated as malformed, since both are real, current, human-authored output.
|
|
36
|
+
const INLINE_FIELD_LIST =
|
|
37
|
+
/Phase:\s*([^|]+?)\s*\|\s*Role:\s*([^|]+?)\s*\|\s*Agent\/Model:\s*([^|]+?)\s*\|\s*Tokens in:\s*([^|]+?)\s*\|\s*Tokens out:\s*([^|]+?)\s*\|\s*Cost:\s*([^|]+?)\s*\|\s*Date:\s*(.+?)\s*$/im
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract every "Token report" entry from a Developer's PR body. Each
|
|
41
|
+
* occurrence of the heading is parsed independently (table form preferred;
|
|
42
|
+
* falls back to the inline field-list form when no table follows), and
|
|
43
|
+
* every occurrence found contributes a row — a re-pushed PR undercounts if
|
|
44
|
+
* only the first/last match is read, per this task's own Traps section.
|
|
45
|
+
*/
|
|
46
|
+
export function parseTokenReportEntries(body: string): LedgerRow[] {
|
|
47
|
+
const lines = body.split(/\r?\n/)
|
|
48
|
+
const headingIdxs: number[] = []
|
|
49
|
+
lines.forEach((line, i) => {
|
|
50
|
+
if (TOKEN_REPORT_HEADING.test(line)) headingIdxs.push(i)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const out: LedgerRow[] = []
|
|
54
|
+
for (let h = 0; h < headingIdxs.length; h++) {
|
|
55
|
+
const start = (headingIdxs[h] as number) + 1
|
|
56
|
+
const end = h + 1 < headingIdxs.length ? (headingIdxs[h + 1] as number) : lines.length
|
|
57
|
+
const section = lines.slice(start, end).join('\n')
|
|
58
|
+
|
|
59
|
+
const tableRows = parseTableSection(section)
|
|
60
|
+
if (tableRows.length > 0) {
|
|
61
|
+
out.push(...tableRows)
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
const inlineRow = parseInlineFieldList(section)
|
|
65
|
+
if (inlineRow) out.push(inlineRow)
|
|
66
|
+
}
|
|
67
|
+
return out
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Same table-body-reading contract as `parse-registry.ts`'s `readMarkdownTable`, minus the heading search (already sliced to a single "Token report" section, which may repeat within one body — the thing `readMarkdownTable` cannot express). */
|
|
71
|
+
function parseTableSection(section: string): LedgerRow[] {
|
|
72
|
+
const lines = section.split(/\r?\n/)
|
|
73
|
+
let i = 0
|
|
74
|
+
for (; i < lines.length; i++) {
|
|
75
|
+
if ((lines[i] ?? '').trim().startsWith('|')) break
|
|
76
|
+
}
|
|
77
|
+
if (i + 1 >= lines.length) return []
|
|
78
|
+
i += 2 // skip header + separator row
|
|
79
|
+
const out: LedgerRow[] = []
|
|
80
|
+
for (; i < lines.length; i++) {
|
|
81
|
+
const trimmed = (lines[i] ?? '').trim()
|
|
82
|
+
if (!trimmed.startsWith('|')) break
|
|
83
|
+
const row = rowFromCells(splitTableRow(trimmed))
|
|
84
|
+
if (row) out.push(row)
|
|
85
|
+
}
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseInlineFieldList(section: string): LedgerRow | null {
|
|
90
|
+
const m = section.match(INLINE_FIELD_LIST)
|
|
91
|
+
if (!m) return null
|
|
92
|
+
return rowFromCells([m[1], m[2], m[3], m[4], m[5], m[6], m[7]] as string[])
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// `Tokens: <phase> — <Role> — <model> — <in>/<out>/<cost or — if unknown>`
|
|
96
|
+
// (`roles/reviewer.md`, `roles/security.md`, `roles/planner.md` — identical
|
|
97
|
+
// shape, phase/role text differs per role). Segments are separated by a dash
|
|
98
|
+
// with a space on both sides so an agent/model name's own internal hyphens
|
|
99
|
+
// (`claude-sonnet-5`) never split — a real separator always has surrounding
|
|
100
|
+
// whitespace. Tolerates `-`/`–` alongside the docs' own `—`, since no real
|
|
101
|
+
// verdict-comment example exists yet to confirm which glyph a live Reviewer
|
|
102
|
+
// turn actually reproduces.
|
|
103
|
+
const TOKENS_LINE_PREFIX = /^\s*(?:\*\*)?Tokens:(?:\*\*)?\s*(.+)$/i
|
|
104
|
+
const SEGMENT_SEP = /\s+[—–-]\s+/
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Extract every Reviewer/Security/Planner `Tokens: …` line from a body of
|
|
108
|
+
* text (a verdict comment, a PR body, or a planning report). A re-review or
|
|
109
|
+
* re-planning reports again without editing the prior line, so more than one
|
|
110
|
+
* match is expected and every one is returned. A line that doesn't split
|
|
111
|
+
* into exactly the 4 documented segments is skipped, not guessed at — same
|
|
112
|
+
* discipline as `parse-ledger.ts`'s malformed-row tolerance.
|
|
113
|
+
*/
|
|
114
|
+
export function parseTokensLines(text: string): LedgerRow[] {
|
|
115
|
+
const out: LedgerRow[] = []
|
|
116
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
117
|
+
const m = rawLine.match(TOKENS_LINE_PREFIX)
|
|
118
|
+
if (!m) continue
|
|
119
|
+
const rest = (m[1] as string).trim()
|
|
120
|
+
const segments = rest.split(SEGMENT_SEP)
|
|
121
|
+
if (segments.length !== 4) continue
|
|
122
|
+
const [phase, role, agentModel, numbers] = segments as [string, string, string, string]
|
|
123
|
+
const [tokensInCell, tokensOutCell, costCell] = splitNumbers(numbers)
|
|
124
|
+
const row = rowFromCells([phase.trim(), role.trim(), agentModel.trim(), tokensInCell, tokensOutCell, costCell, ''])
|
|
125
|
+
if (row) out.push(row)
|
|
126
|
+
}
|
|
127
|
+
return out
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function splitNumbers(numbers: string): [string, string, string] {
|
|
131
|
+
const trimmed = numbers.trim()
|
|
132
|
+
if (isDashOnly(trimmed)) return ['—', '—', '—']
|
|
133
|
+
const parts = trimmed.split('/')
|
|
134
|
+
return [parts[0] ?? '—', parts[1] ?? '—', parts[2] ?? '—']
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isDashOnly(s: string): boolean {
|
|
138
|
+
return s === '—' || s === '-' || s === '–'
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** One merged PR's body + top-level comments — the caller's fetch result, no I/O here. */
|
|
142
|
+
export type TokenSourcePr = {
|
|
143
|
+
number: number
|
|
144
|
+
body: string
|
|
145
|
+
comments: string[]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Pure aggregation over every merged PR associated with a task (its own
|
|
150
|
+
* branch's PR(s), plus any plan/cross-referenced PR carrying the Planner's
|
|
151
|
+
* report) — Developer "Token report" entries and `Tokens: …` lines from
|
|
152
|
+
* each PR body, and `Tokens: …` lines from each PR's comments. Missing or
|
|
153
|
+
* malformed reports simply produce no row for that report — never a
|
|
154
|
+
* fabricated figure — matching `archivist.md`'s own "flag it under DANGLING
|
|
155
|
+
* instead" discipline; the caller decides how to surface an empty result.
|
|
156
|
+
*/
|
|
157
|
+
export function aggregateTaskTokenRows(prs: TokenSourcePr[]): LedgerRow[] {
|
|
158
|
+
const out: LedgerRow[] = []
|
|
159
|
+
for (const pr of prs) {
|
|
160
|
+
out.push(...parseTokenReportEntries(pr.body))
|
|
161
|
+
out.push(...parseTokensLines(pr.body))
|
|
162
|
+
for (const comment of pr.comments) out.push(...parseTokensLines(comment))
|
|
163
|
+
}
|
|
164
|
+
return out
|
|
165
|
+
}
|