@smartmemory/compose 0.3.6-beta → 0.3.8
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/.claude/skills/compose/SKILL.md +42 -88
- package/.compose-deps.json +1 -13
- package/README.md +72 -5
- package/bin/compose.js +754 -347
- package/bin/git-hooks/pre-push.template +29 -0
- package/bin/judgment-import.js +7 -0
- package/bin/judgment-migrate.js +387 -0
- package/contracts/comp-obs-contract.schema.json +9 -3
- package/contracts/feature-json.schema.json +5 -0
- package/contracts/fluid-record.schema.json +209 -0
- package/contracts/judgment-record.schema.json +425 -4
- package/contracts/lifecycle-backfill.schema.json +322 -0
- package/dist/assets/App-Z4MU-H_F.js +916 -0
- package/dist/assets/_baseUniq-ClWoCPFl.js +1 -0
- package/dist/assets/arc-DY26UIVo.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-6Ggq4DqJ.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-CH3Ked0l.js +132 -0
- package/dist/assets/{browser-BSM23If2.js → browser-BWkrenen.js} +6 -6
- package/dist/assets/{c4Diagram-LMCZKHZV-DZf45Fbz.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
- package/dist/assets/channel-SnZzzh7k.js +1 -0
- package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-JytR14a9.js +206 -0
- package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-BySQzVxc.js +231 -0
- package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-pURGrJDk.js → chunk-YZCP3GAM-iE_tzriw.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +1 -0
- package/dist/assets/clone-DgklGjHm.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-BieYif4o.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
- package/dist/assets/dagre-KV5264BT-Cp3F5KTn.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-DiR6_2q_.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-w0i-p5HX.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-tIHhwUv3.js +43 -0
- package/dist/assets/diagram-TYMM5635-BAeY3B19.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-Ckx_Knko.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-DeoNka6J.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-BmGnFbEg.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-Dk48IHsx.js +106 -0
- package/dist/assets/graph-BNzKGvoy.js +1 -0
- package/dist/assets/graph-CI_1htl0.js +331 -0
- package/dist/assets/index-BEfrNBp8.js +123 -0
- package/dist/assets/index-yyrA5OZd.css +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-BRf827i0.js +2 -0
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +5 -5
- package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +7 -7
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-BI8cXFPI.js +1 -0
- package/dist/assets/{linear-Ck7gpa5N.js → linear-a0glcDiw.js} +1 -1
- package/dist/assets/min-vPHfnXcC.js +1 -0
- package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +7 -7
- package/dist/assets/mobile-B7m9EO9D.js +17 -0
- package/dist/assets/pieDiagram-DEJITSTG-Cno-gETh.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-BUQM1Hfm.js +7 -0
- package/dist/assets/{requirementDiagram-TGXJPOKE-bnI2zJeT.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +3 -3
- package/dist/assets/sankeyDiagram-XADWPNL6-Crynd3_b.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-D9fZdCM8.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-CW9qVec8.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-D267GQFF.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +3 -3
- package/dist/assets/vennDiagram-DHZGUBPP-BfytJcWk.js +34 -0
- package/dist/assets/wardley-RL74JXVD-DLj-IjyB.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-Ds0Ue68c.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-vjWDXFL6.js +7 -0
- package/dist/index.html +3 -3
- package/lib/agent-string.js +7 -5
- package/lib/append-integrity.js +81 -0
- package/lib/backfill-evidence.js +109 -0
- package/lib/bug-escalation.js +39 -4
- package/lib/build-stream-schema.js +3 -1
- package/lib/build-stream-writer.js +25 -0
- package/lib/build.js +1624 -195
- package/lib/canon-guard.js +245 -0
- package/lib/canon-override.js +196 -0
- package/lib/canon-registry.js +291 -0
- package/lib/cli-commands.js +144 -0
- package/lib/codex-preflight.js +50 -15
- package/lib/colleague/context.js +215 -0
- package/lib/colleague/writeback.js +95 -0
- package/lib/completion-gate.js +1421 -0
- package/lib/completion-writer.js +47 -47
- package/lib/consumer-fanout.js +105 -11
- package/lib/coverage-gate.js +200 -0
- package/lib/dir-lock.js +170 -0
- package/lib/dispatch-ledger.js +301 -0
- package/lib/dispatch-metrics.js +236 -0
- package/lib/experiment-judge.js +6 -1
- package/lib/feature-json.js +1 -1
- package/lib/feature-reconciler.js +8 -0
- package/lib/feature-validator.js +64 -1
- package/lib/feature-writer.js +66 -2
- package/lib/fluid/factory.js +167 -0
- package/lib/fluid/ideabox-dates.js +73 -0
- package/lib/fluid/ideabox-migrate.js +154 -0
- package/lib/fluid/ideabox-ops.js +585 -0
- package/lib/fluid/ideabox-view.js +146 -0
- package/lib/fluid/import-ideabox.js +186 -0
- package/lib/fluid/local-provider.js +606 -0
- package/lib/fluid/provider.js +684 -0
- package/lib/fluid/record-shape.js +214 -0
- package/lib/fluid/record-store.js +328 -0
- package/lib/fluid/render-ideabox.js +261 -0
- package/lib/fluid/schema.js +40 -0
- package/lib/fluid/smartmemory-provider.js +1695 -0
- package/lib/gsd.js +63 -14
- package/lib/guard-cli.js +175 -0
- package/lib/guard-custody.js +141 -0
- package/lib/guard-descriptors.js +530 -0
- package/lib/guard-enrol.js +254 -0
- package/lib/health-score.js +1 -1
- package/lib/hooks-status.js +32 -3
- package/lib/ideabox-cli.js +315 -0
- package/lib/ideabox.js +121 -21
- package/lib/judgment/store/index.js +166 -0
- package/lib/judgment/store/records.js +184 -25
- package/lib/judgment/trace.js +380 -0
- package/lib/judgment-attest.js +259 -0
- package/lib/judgment-decision-write.js +277 -0
- package/lib/judgment-decisions.js +466 -0
- package/lib/judgment-gen.js +375 -22
- package/lib/judgment-verify.js +153 -0
- package/lib/judgment-writer.js +2842 -262
- package/lib/lane-gate.js +2 -0
- package/lib/lifecycle-modes.js +4 -4
- package/lib/lineage.js +400 -0
- package/lib/local-claude-connector.js +250 -54
- package/lib/maya-client.js +302 -0
- package/lib/maya-config.js +53 -0
- package/lib/maya-identity.js +283 -0
- package/lib/mcp-enforcement.js +21 -35
- package/lib/migrate-anon.js +5 -0
- package/lib/migrate-roadmap.js +15 -0
- package/lib/new.js +13 -1
- package/lib/pipeline-compat.js +104 -0
- package/lib/policy-catalog.js +295 -0
- package/lib/policy-check.js +0 -0
- package/lib/process-termination.js +98 -0
- package/lib/resolve-workspace.js +5 -1
- package/lib/result-normalizer.js +428 -153
- package/lib/review-normalize.js +4 -0
- package/lib/roadmap-errors.js +65 -0
- package/lib/roadmap-preservers.js +24 -4
- package/lib/roadmap-residue.js +299 -0
- package/lib/smartmemory-client.js +614 -78
- package/lib/smartmemory-config.js +54 -0
- package/lib/smartmemory-ingest.js +19 -2
- package/lib/step-prompt.js +7 -6
- package/lib/stratum-engine.js +53 -4
- package/lib/stratum-mcp-client.js +391 -31
- package/lib/test-bootstrap.js +31 -0
- package/lib/tool-inventory.js +122 -0
- package/lib/version-check.js +91 -19
- package/lib/vision-writer.js +88 -1
- package/package.json +7 -6
- package/pipelines/bug-fix.stratum.yaml +205 -211
- package/pipelines/build-quick.profiles.json +12 -0
- package/pipelines/build-quick.stratum.yaml +263 -350
- package/pipelines/content.stratum.yaml +81 -77
- package/pipelines/coverage-sweep.stratum.yaml +49 -30
- package/pipelines/plan.stratum.yaml +76 -86
- package/pipelines/refactor.stratum.yaml +125 -125
- package/pipelines/research.stratum.yaml +56 -58
- package/pipelines/review-fix.profiles.json +6 -0
- package/pipelines/review-fix.stratum.yaml +110 -83
- package/presets/team-feature.profiles.json +6 -0
- package/presets/team-feature.stratum.yaml +93 -66
- package/presets/team-research.profiles.json +6 -0
- package/presets/team-research.stratum.yaml +89 -80
- package/presets/team-review.profiles.json +8 -0
- package/presets/team-review.stratum.yaml +98 -80
- package/scripts/cost-census.mjs +70 -0
- package/scripts/guard-sign/compose-guard-sign.sh +62 -0
- package/server/agent-health.js +22 -0
- package/server/agent-hooks.js +14 -1
- package/server/agent-server.js +5 -248
- package/server/agent-spawn.js +3 -4
- package/server/agent-workspace.js +294 -0
- package/server/build-routes.js +6 -5
- package/server/build-stream-bridge.js +53 -0
- package/server/cc-session-watcher.js +4 -1
- package/server/coalescing-buffer.js +7 -1
- package/server/completion-projection.js +228 -0
- package/server/compose-mcp-tools.js +124 -24
- package/server/compose-mcp.js +91 -790
- package/server/decision-event-emit.js +41 -2
- package/server/decision-event-id.js +17 -0
- package/server/decision-events-snapshot.js +3 -0
- package/server/design-routes.js +14 -8
- package/server/feature-scan.js +76 -2
- package/server/file-watcher.js +170 -21
- package/server/ideabox-routes.js +166 -224
- package/server/index.js +70 -100
- package/server/lifecycle-guard.js +240 -10
- package/server/lifecycle-phase-history.js +276 -0
- package/server/maya-routes.js +507 -0
- package/server/mcp-tool-defs.js +940 -0
- package/server/mcp-tool-policy.js +35 -3
- package/server/model-tiers.js +22 -5
- package/server/pipeline-routes.js +21 -11
- package/server/project-root.js +58 -19
- package/server/remote-utils.js +3 -1
- package/server/schema-validator.js +7 -1
- package/server/session-manager.js +5 -6
- package/server/session-routes.js +3 -1
- package/server/stratum-client.js +57 -10
- package/server/stratum-sync.js +6 -3
- package/server/summarizer.js +3 -4
- package/server/supervisor.js +0 -1
- package/server/vision-routes.js +208 -98
- package/server/vision-server.js +86 -23
- package/server/vision-store.js +60 -6
- package/server/vision-utils.js +3 -4
- package/server/workspace-activity.js +18 -0
- package/server/workspace-middleware.js +2 -2
- package/server/workspace-runtime.js +243 -0
- package/server/worktree-gc.js +1 -0
- package/dist/assets/App-BG3ngu8H.js +0 -896
- package/dist/assets/abnfDiagram-VRR7QNED-CjB_sD3D.js +0 -1
- package/dist/assets/arc-_v4hR_uD.js +0 -1
- package/dist/assets/architectureDiagram-ZJ3FMSHR-DreJmzXQ.js +0 -36
- package/dist/assets/blockDiagram-677ZJIJ3-BG9-c0O1.js +0 -132
- package/dist/assets/channel-B3U5wFAT.js +0 -1
- package/dist/assets/chunk-EX3LRPZG-DdELs1qP.js +0 -231
- package/dist/assets/chunk-MOJQB5TN-D-ky35G-.js +0 -88
- package/dist/assets/chunk-RYQCIY6F-Dag_kVlO.js +0 -1
- package/dist/assets/chunk-V7JOEXUC-BtewURat.js +0 -206
- package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CT2BA6KE.js +0 -178
- package/dist/assets/cynefinDiagram-TSTJHNR4-Bh6exbyg.js +0 -62
- package/dist/assets/dagre-VKFMJZFB-aXMLSmQL.js +0 -4
- package/dist/assets/diagram-FQU43EPY-Dr7JAOuQ.js +0 -3
- package/dist/assets/diagram-G47NLZAW-DUvA3FQK.js +0 -24
- package/dist/assets/diagram-NH7WQ7WH-BQUARqcu.js +0 -24
- package/dist/assets/diagram-OA4YK3LP-dDUc1zHi.js +0 -30
- package/dist/assets/diagram-WEI45ONY-B2h5Qlb1.js +0 -41
- package/dist/assets/ebnfDiagram-CCIWWBDH-DThRGupB.js +0 -1
- package/dist/assets/erDiagram-Q63AITRT-BUCsprO2.js +0 -85
- package/dist/assets/flowDiagram-23GEKE2U-DXtNNi6r.js +0 -156
- package/dist/assets/ganttDiagram-NO4QXBWP-D4zbBHh_.js +0 -292
- package/dist/assets/gitGraphDiagram-IHSO6WYX-DpoQws0W.js +0 -106
- package/dist/assets/graph-BXPQrYYB.js +0 -331
- package/dist/assets/graph-C9eacEi8.js +0 -1
- package/dist/assets/index-3ZH5eMcZ.js +0 -119
- package/dist/assets/index-LIwREYgH.css +0 -1
- package/dist/assets/infoDiagram-FWYZ7A6U-Bbas2GAo.js +0 -2
- package/dist/assets/katex-C5jXJg4s.js +0 -257
- package/dist/assets/layout-DEXfKzaS.js +0 -1
- package/dist/assets/map-Czzmt4hB.js +0 -1
- package/dist/assets/mobile-CaoXUwAr.js +0 -17
- package/dist/assets/pegDiagram-2B236MQR-CHiINrNy.js +0 -1
- package/dist/assets/pieDiagram-ENE6RG2P-CfS4YFlR.js +0 -39
- package/dist/assets/quadrantDiagram-ABIIQ3AL-CadesS9w.js +0 -7
- package/dist/assets/railroadDiagram-RFXS5EU6-CgWEspBN.js +0 -1
- package/dist/assets/sankeyDiagram-HTMAVEWB-YWKFgOGw.js +0 -40
- package/dist/assets/sequenceDiagram-DBY2YBRQ-BvkNOyF9.js +0 -162
- package/dist/assets/sizeCapture-X5ZJPWSS-DlFPA2yO.js +0 -1
- package/dist/assets/stateDiagram-2N3HPSRC-h8NIx0kQ.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +0 -1
- package/dist/assets/swimlanes-5IMT3BWC-CT5n22kG.js +0 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +0 -8
- package/dist/assets/vennDiagram-L72KCM5P-Dj-wWLYG.js +0 -34
- package/dist/assets/wardleyDiagram-EHGQE667-BxCeYxkG.js +0 -78
- package/dist/assets/xychartDiagram-FW5EYKEG-DMFqWn7z.js +0 -7
- package/lib/staleness.js +0 -87
- package/server/ideabox-cache.js +0 -77
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dispatch-ledger.js — closed, append-only telemetry events for agent dispatch.
|
|
3
|
+
*
|
|
4
|
+
* Ledger rows are addressed by an explicit project root so callers running in a
|
|
5
|
+
* worktree cannot accidentally split telemetry away from the build project.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { dirname, join, resolve } from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { normalizeSince } from './feature-events.js';
|
|
13
|
+
|
|
14
|
+
export const DISPATCH_LEDGER_RELATIVE_PATH = '.compose/data/dispatch-ledger.jsonl';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the project root a dispatch connector should address its ledger by.
|
|
18
|
+
*
|
|
19
|
+
* Two paths write telemetry into the live compose checkout during a test run:
|
|
20
|
+
* an unattributed dispatch with no project_cwd falls back to process.cwd(), and
|
|
21
|
+
* a test that explicitly roots a build at process.cwd() threads that same live
|
|
22
|
+
* path through. Under NODE_TEST_CONTEXT, any ledger write addressed at the
|
|
23
|
+
* process cwd (the live checkout) is redirected to a hermetic tmpdir so
|
|
24
|
+
* full-suite runs never accrete fixture rows in the real .compose/data ledger.
|
|
25
|
+
* A test that threads its own tmpdir project root is addressed there, unchanged.
|
|
26
|
+
* Production paths never set NODE_TEST_CONTEXT, so they are unaffected.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} [explicitCwd] the caller's project_cwd, when known
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
export function resolveDispatchLedgerCwd(explicitCwd) {
|
|
32
|
+
const cwd = isString(explicitCwd) ? explicitCwd : process.cwd();
|
|
33
|
+
if (process.env.NODE_TEST_CONTEXT && resolve(cwd) === resolve(process.cwd())) {
|
|
34
|
+
return join(tmpdir(), 'compose-test-worktrees', 'dispatch-ledger');
|
|
35
|
+
}
|
|
36
|
+
return cwd;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const DISPATCH_SITES = Object.freeze([
|
|
40
|
+
'build-step', 'consumer', 'review', 'review-repair', 'policy-revision', 'gsd',
|
|
41
|
+
'gate-qa', 'escalation', 'preflight', 'judge', 'design-chat', 'import',
|
|
42
|
+
'validation', 'new-project', 'unattributed',
|
|
43
|
+
]);
|
|
44
|
+
export const DISPATCH_OUTCOMES = Object.freeze(['ok', 'error', 'blocked']);
|
|
45
|
+
export const SETTLEMENT_FAILURE_CLASSES = Object.freeze([
|
|
46
|
+
'ownership', 'vocabulary', 'normalization', 'agent', 'ensure-retry',
|
|
47
|
+
]);
|
|
48
|
+
export const ESTIMATE_LANES = Object.freeze(['trivial', 'standard', 'complex']);
|
|
49
|
+
export const ESTIMATE_SOURCES = Object.freeze(['fresh', 'cached', 'escalated']);
|
|
50
|
+
export const TRIAGE_CONFIDENCES = Object.freeze(['high', 'medium', 'low']);
|
|
51
|
+
export const BUILD_TERMINAL_STATUSES = Object.freeze(['complete', 'failed', 'aborted']);
|
|
52
|
+
export const BUILD_FILES_SOURCES = Object.freeze(['ship', 'accumulated']);
|
|
53
|
+
|
|
54
|
+
const EVENT_FIELDS = {
|
|
55
|
+
dispatch: {
|
|
56
|
+
required: ['dispatch_id', 'site', 'agent', 'outcome'],
|
|
57
|
+
optional: [
|
|
58
|
+
'build_id', 'feature_code', 'step_id', 'attempt', 'model',
|
|
59
|
+
'effort_intended', 'effort_executed', 'tokens_in', 'tokens_out',
|
|
60
|
+
'tokens_total', 'usd', 'duration_ms', 'note',
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
settlement: {
|
|
64
|
+
required: ['dispatch_id', 'accepted'],
|
|
65
|
+
optional: ['build_id', 'step_id', 'failure_class'],
|
|
66
|
+
},
|
|
67
|
+
'triage-estimate': {
|
|
68
|
+
required: ['build_id', 'feature_code', 'tier', 'lane', 'profile', 'estimate_source'],
|
|
69
|
+
optional: ['confidence'],
|
|
70
|
+
},
|
|
71
|
+
'build-actuals': {
|
|
72
|
+
required: [
|
|
73
|
+
'build_id', 'feature_code', 'terminal_status', 'files_changed_count',
|
|
74
|
+
'files_source', 'review_iterations', 'escalations', 'tokens_total', 'usd',
|
|
75
|
+
],
|
|
76
|
+
optional: ['test_count', 'pass_rate'],
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const countersByPath = new Map();
|
|
81
|
+
|
|
82
|
+
function ledgerPath(projectCwd) {
|
|
83
|
+
return resolve(projectCwd, DISPATCH_LEDGER_RELATIVE_PATH);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isPlainObject(value) {
|
|
87
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isString(value) {
|
|
91
|
+
return typeof value === 'string' && value.length > 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isFiniteNumber(value) {
|
|
95
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isNullableFiniteNumber(value) {
|
|
99
|
+
return value === null || isFiniteNumber(value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isNullableString(value) {
|
|
103
|
+
return value === null || isString(value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isEnum(value, values) {
|
|
107
|
+
return values.includes(value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function invalid(message) {
|
|
111
|
+
throw new Error(`dispatch-ledger.appendEvent: ${message}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function validateEvent(event, { allowExtra = false } = {}) {
|
|
115
|
+
if (!isPlainObject(event)) invalid('event must be an object');
|
|
116
|
+
if (!isString(event.kind)) invalid('kind is required');
|
|
117
|
+
const shape = EVENT_FIELDS[event.kind];
|
|
118
|
+
if (!shape) invalid(`unknown kind "${event.kind}"`);
|
|
119
|
+
|
|
120
|
+
const allowed = new Set(['kind', ...shape.required, ...shape.optional]);
|
|
121
|
+
if (!allowExtra) {
|
|
122
|
+
for (const field of Object.keys(event)) {
|
|
123
|
+
if (!allowed.has(field)) invalid(`unknown field "${field}" for ${event.kind}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const field of shape.required) {
|
|
127
|
+
if (!Object.hasOwn(event, field) || event[field] === undefined) {
|
|
128
|
+
invalid(`required field "${field}" is missing for ${event.kind}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
switch (event.kind) {
|
|
133
|
+
case 'dispatch':
|
|
134
|
+
requireString(event, 'dispatch_id');
|
|
135
|
+
requireEnum(event, 'site', DISPATCH_SITES);
|
|
136
|
+
requireString(event, 'agent');
|
|
137
|
+
requireEnum(event, 'outcome', DISPATCH_OUTCOMES);
|
|
138
|
+
optionalString(event, 'build_id');
|
|
139
|
+
optionalString(event, 'feature_code');
|
|
140
|
+
optionalString(event, 'step_id');
|
|
141
|
+
optionalNumber(event, 'attempt');
|
|
142
|
+
optionalNullableString(event, 'model');
|
|
143
|
+
optionalNullableString(event, 'effort_intended');
|
|
144
|
+
optionalNullableString(event, 'effort_executed');
|
|
145
|
+
for (const field of ['tokens_in', 'tokens_out', 'tokens_total', 'usd', 'duration_ms']) {
|
|
146
|
+
optionalNullableNumber(event, field);
|
|
147
|
+
}
|
|
148
|
+
optionalString(event, 'note');
|
|
149
|
+
break;
|
|
150
|
+
case 'settlement':
|
|
151
|
+
requireString(event, 'dispatch_id');
|
|
152
|
+
if (typeof event.accepted !== 'boolean') invalid('accepted must be a boolean');
|
|
153
|
+
optionalString(event, 'build_id');
|
|
154
|
+
optionalString(event, 'step_id');
|
|
155
|
+
optionalEnum(event, 'failure_class', SETTLEMENT_FAILURE_CLASSES);
|
|
156
|
+
break;
|
|
157
|
+
case 'triage-estimate':
|
|
158
|
+
requireString(event, 'build_id');
|
|
159
|
+
requireString(event, 'feature_code');
|
|
160
|
+
if (!Number.isInteger(event.tier) || event.tier < 0 || event.tier > 4) {
|
|
161
|
+
invalid('tier must be an integer 0-4');
|
|
162
|
+
}
|
|
163
|
+
requireEnum(event, 'lane', ESTIMATE_LANES);
|
|
164
|
+
if (!isPlainObject(event.profile)) invalid('profile must be a non-array object');
|
|
165
|
+
requireEnum(event, 'estimate_source', ESTIMATE_SOURCES);
|
|
166
|
+
if (Object.hasOwn(event, 'confidence') && event.confidence !== null && !isEnum(event.confidence, TRIAGE_CONFIDENCES)) {
|
|
167
|
+
invalid(`confidence must be one of ${TRIAGE_CONFIDENCES.join(', ')} or null`);
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
case 'build-actuals':
|
|
171
|
+
requireString(event, 'build_id');
|
|
172
|
+
requireString(event, 'feature_code');
|
|
173
|
+
requireEnum(event, 'terminal_status', BUILD_TERMINAL_STATUSES);
|
|
174
|
+
requireNumber(event, 'files_changed_count');
|
|
175
|
+
requireEnum(event, 'files_source', BUILD_FILES_SOURCES);
|
|
176
|
+
requireNumber(event, 'review_iterations');
|
|
177
|
+
requireNumber(event, 'escalations');
|
|
178
|
+
requireNumber(event, 'tokens_total');
|
|
179
|
+
requireNumber(event, 'usd');
|
|
180
|
+
optionalNullableNumber(event, 'test_count');
|
|
181
|
+
optionalNullableNumber(event, 'pass_rate');
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function requireString(event, field) {
|
|
187
|
+
if (!isString(event[field])) invalid(`${field} must be a non-empty string`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function requireNumber(event, field) {
|
|
191
|
+
if (!isFiniteNumber(event[field])) invalid(`${field} must be a number`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function requireEnum(event, field, values) {
|
|
195
|
+
if (!isEnum(event[field], values)) invalid(`${field} must be one of ${values.join(', ')}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function optionalString(event, field) {
|
|
199
|
+
if (Object.hasOwn(event, field) && !isString(event[field])) invalid(`${field} must be a non-empty string`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function optionalNullableString(event, field) {
|
|
203
|
+
if (Object.hasOwn(event, field) && !isNullableString(event[field])) invalid(`${field} must be a string or null`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function optionalNumber(event, field) {
|
|
207
|
+
if (Object.hasOwn(event, field) && !isFiniteNumber(event[field])) invalid(`${field} must be a number`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function optionalNullableNumber(event, field) {
|
|
211
|
+
if (Object.hasOwn(event, field) && !isNullableFiniteNumber(event[field])) invalid(`${field} must be a number or null`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function optionalEnum(event, field, values) {
|
|
215
|
+
if (Object.hasOwn(event, field) && !isEnum(event[field], values)) {
|
|
216
|
+
invalid(`${field} must be one of ${values.join(', ')}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isValidStoredRow(row) {
|
|
221
|
+
if (!isPlainObject(row) || row.v !== 1 || !isIsoTimestamp(row.ts) || !Number.isInteger(row._seq) || row._seq < 0) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
validateEvent(row, { allowExtra: true });
|
|
226
|
+
return true;
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isIsoTimestamp(value) {
|
|
233
|
+
return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value) && !Number.isNaN(Date.parse(value));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function seedCounter(path) {
|
|
237
|
+
if (countersByPath.has(path)) return countersByPath.get(path);
|
|
238
|
+
let highest = 0;
|
|
239
|
+
if (existsSync(path)) {
|
|
240
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
241
|
+
if (!line.trim()) continue;
|
|
242
|
+
try {
|
|
243
|
+
const row = JSON.parse(line);
|
|
244
|
+
if (isValidStoredRow(row)) highest = Math.max(highest, row._seq);
|
|
245
|
+
} catch {
|
|
246
|
+
// A torn JSONL row must not prevent later rows from being read.
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
countersByPath.set(path, highest);
|
|
251
|
+
return highest;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Append one closed dispatch-ledger event, stamping its immutable envelope.
|
|
256
|
+
*
|
|
257
|
+
* @param {string} projectCwd
|
|
258
|
+
* @param {object} event caller-supplied kind and closed payload only
|
|
259
|
+
* @returns {object} the row written to disk
|
|
260
|
+
*/
|
|
261
|
+
export function appendEvent(projectCwd, event) {
|
|
262
|
+
validateEvent(event);
|
|
263
|
+
const path = ledgerPath(projectCwd);
|
|
264
|
+
const nextSeq = seedCounter(path) + 1;
|
|
265
|
+
countersByPath.set(path, nextSeq);
|
|
266
|
+
const row = { v: 1, ts: new Date().toISOString(), _seq: nextSeq, ...event };
|
|
267
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
268
|
+
appendFileSync(path, JSON.stringify(row) + '\n');
|
|
269
|
+
return row;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Tolerantly read valid, known ledger rows. Future fields are preserved.
|
|
274
|
+
*
|
|
275
|
+
* @param {string} projectCwd
|
|
276
|
+
* @param {object} [opts]
|
|
277
|
+
* @param {string} [opts.kind]
|
|
278
|
+
* @param {string|number|Date} [opts.since]
|
|
279
|
+
* @param {string} [opts.feature]
|
|
280
|
+
* @returns {Array<object>}
|
|
281
|
+
*/
|
|
282
|
+
export function readEvents(projectCwd, { kind, since, feature } = {}) {
|
|
283
|
+
const path = ledgerPath(projectCwd);
|
|
284
|
+
if (!existsSync(path)) return [];
|
|
285
|
+
const sinceMs = normalizeSince(since);
|
|
286
|
+
const rows = [];
|
|
287
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
288
|
+
if (!line.trim()) continue;
|
|
289
|
+
try {
|
|
290
|
+
const row = JSON.parse(line);
|
|
291
|
+
if (!isValidStoredRow(row)) continue;
|
|
292
|
+
if (kind && row.kind !== kind) continue;
|
|
293
|
+
if (feature && row.feature_code !== feature) continue;
|
|
294
|
+
if (sinceMs !== null && Date.parse(row.ts) < sinceMs) continue;
|
|
295
|
+
rows.push(row);
|
|
296
|
+
} catch {
|
|
297
|
+
// Malformed rows are expected in an append-only JSONL file after a tear.
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return rows;
|
|
301
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dispatch-metrics.js — read-only aggregation over the dispatch ledger.
|
|
3
|
+
*
|
|
4
|
+
* This module deliberately keeps the ledger's append-only observations intact:
|
|
5
|
+
* settlements and terminal actuals are reduced at read time by their `_seq`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readEvents } from './dispatch-ledger.js';
|
|
9
|
+
|
|
10
|
+
const RETRY_NOTE = 'known undercount: child flows';
|
|
11
|
+
const GSD_NOTE = 'not instrumented';
|
|
12
|
+
|
|
13
|
+
function isNumber(value) {
|
|
14
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function upperMiddle(values) {
|
|
18
|
+
const sorted = values.filter(isNumber).sort((a, b) => a - b);
|
|
19
|
+
return sorted.length ? sorted[Math.floor(sorted.length / 2)] : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function rate(numerator, denominator) {
|
|
23
|
+
return denominator > 0 ? numerator / denominator : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function highestBy(rows, key) {
|
|
27
|
+
const result = new Map();
|
|
28
|
+
for (const row of rows) {
|
|
29
|
+
const current = result.get(row[key]);
|
|
30
|
+
if (!current || row._seq > current._seq) result.set(row[key], row);
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function compareNullable(a, b) {
|
|
36
|
+
if (a === b) return 0;
|
|
37
|
+
if (a === null || a === undefined) return -1;
|
|
38
|
+
if (b === null || b === undefined) return 1;
|
|
39
|
+
return String(a).localeCompare(String(b));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function collectRowStats(dispatches, settlements, { gsd = false } = {}) {
|
|
43
|
+
const tokenValues = dispatches.map((row) => row.tokens_total).filter(isNumber);
|
|
44
|
+
const durationValues = dispatches.map((row) => row.duration_ms).filter(isNumber);
|
|
45
|
+
const settled = dispatches
|
|
46
|
+
.map((row) => settlements.get(row.dispatch_id))
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
const accepted = settled.filter((row) => row.accepted).length;
|
|
49
|
+
const retryEligible = dispatches.filter((row) => isNumber(row.attempt));
|
|
50
|
+
const retries = retryEligible.filter((row) => row.attempt > 1).length;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
token_sample_count: tokenValues.length,
|
|
54
|
+
null_usage_count: dispatches.filter((row) => !isNumber(row.tokens_total)).length,
|
|
55
|
+
median_tokens: upperMiddle(tokenValues),
|
|
56
|
+
duration_sample_count: durationValues.length,
|
|
57
|
+
median_duration_ms: upperMiddle(durationValues),
|
|
58
|
+
completion: {
|
|
59
|
+
ok: dispatches.filter((row) => row.outcome === 'ok').length,
|
|
60
|
+
total: dispatches.length,
|
|
61
|
+
rate: rate(dispatches.filter((row) => row.outcome === 'ok').length, dispatches.length),
|
|
62
|
+
},
|
|
63
|
+
acceptance: {
|
|
64
|
+
accepted,
|
|
65
|
+
settled: settled.length,
|
|
66
|
+
rate: rate(accepted, settled.length),
|
|
67
|
+
note: gsd ? GSD_NOTE : null,
|
|
68
|
+
},
|
|
69
|
+
retry: {
|
|
70
|
+
retries,
|
|
71
|
+
eligible: retryEligible.length,
|
|
72
|
+
rate: rate(retries, retryEligible.length),
|
|
73
|
+
note: RETRY_NOTE,
|
|
74
|
+
},
|
|
75
|
+
usd_total: dispatches.reduce((total, row) => total + (isNumber(row.usd) ? row.usd : 0), 0),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function realizedLane(actual) {
|
|
80
|
+
if (actual.escalations >= 1 || actual.review_iterations >= 3 || actual.files_changed_count >= 6) return 'complex';
|
|
81
|
+
if (actual.files_changed_count <= 2 && actual.review_iterations <= 1 && actual.escalations === 0) return 'trivial';
|
|
82
|
+
return 'standard';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function acrrSummary(rows) {
|
|
86
|
+
const total = rows.length;
|
|
87
|
+
const matched = rows.filter((row) => row.matched).length;
|
|
88
|
+
return { matched, total, rate: rate(matched, total) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Aggregate dispatch, settlement, estimate, and terminal actual rows without
|
|
93
|
+
* mutating the ledger.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} projectCwd
|
|
96
|
+
* @param {{ since?: string|number|Date, feature?: string }} [opts]
|
|
97
|
+
* @returns {object}
|
|
98
|
+
*/
|
|
99
|
+
export function collectDispatchMetrics(projectCwd, { since, feature } = {}) {
|
|
100
|
+
// Settlements have no feature code. Read one since-filtered population first,
|
|
101
|
+
// then join them to the feature-filtered dispatches below.
|
|
102
|
+
const allRows = readEvents(projectCwd, { since });
|
|
103
|
+
const inFeature = (row) => !feature || row.feature_code === feature;
|
|
104
|
+
const dispatches = allRows.filter((row) => row.kind === 'dispatch' && inFeature(row));
|
|
105
|
+
const settlements = highestBy(allRows.filter((row) => row.kind === 'settlement'), 'dispatch_id');
|
|
106
|
+
|
|
107
|
+
const modelBuckets = new Map();
|
|
108
|
+
for (const row of dispatches) {
|
|
109
|
+
const executed = row.effort_executed ?? null;
|
|
110
|
+
const intendedWhenUnknown = executed === null ? (row.effort_intended ?? null) : null;
|
|
111
|
+
const values = [row.model ?? null, executed, intendedWhenUnknown];
|
|
112
|
+
const key = JSON.stringify(values);
|
|
113
|
+
if (!modelBuckets.has(key)) modelBuckets.set(key, { values, rows: [] });
|
|
114
|
+
modelBuckets.get(key).rows.push(row);
|
|
115
|
+
}
|
|
116
|
+
const model_effort = [...modelBuckets.values()]
|
|
117
|
+
.sort((a, b) => compareNullable(a.values[0], b.values[0]) || compareNullable(a.values[1], b.values[1]) || compareNullable(a.values[2], b.values[2]))
|
|
118
|
+
.map(({ values: [model, effort_executed, effort_intended_when_unknown], rows }) => {
|
|
119
|
+
const stats = collectRowStats(rows, settlements, { gsd: rows.every((row) => row.site === 'gsd') });
|
|
120
|
+
return {
|
|
121
|
+
model, effort_executed, effort_intended_when_unknown, dispatch_count: rows.length,
|
|
122
|
+
...stats,
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const siteBuckets = new Map();
|
|
127
|
+
for (const row of dispatches) {
|
|
128
|
+
if (!siteBuckets.has(row.site)) siteBuckets.set(row.site, []);
|
|
129
|
+
siteBuckets.get(row.site).push(row);
|
|
130
|
+
}
|
|
131
|
+
const sites = [...siteBuckets.entries()]
|
|
132
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
133
|
+
.map(([site, rows]) => ({ site, dispatch_count: rows.length, ...collectRowStats(rows, settlements, { gsd: site === 'gsd' }) }));
|
|
134
|
+
|
|
135
|
+
const estimates = allRows.filter((row) => row.kind === 'triage-estimate' && inFeature(row));
|
|
136
|
+
const actualRows = allRows.filter((row) => row.kind === 'build-actuals' && inFeature(row));
|
|
137
|
+
const actuals = highestBy(actualRows, 'build_id');
|
|
138
|
+
const estimateBuildIds = new Set(estimates.map((row) => row.build_id));
|
|
139
|
+
const acrrRows = estimates
|
|
140
|
+
.map((estimate) => {
|
|
141
|
+
const actual = actuals.get(estimate.build_id) ?? null;
|
|
142
|
+
const eligible = actual?.terminal_status === 'complete';
|
|
143
|
+
const lane = eligible ? realizedLane(actual) : null;
|
|
144
|
+
return {
|
|
145
|
+
build_id: estimate.build_id,
|
|
146
|
+
feature_code: estimate.feature_code,
|
|
147
|
+
estimate_source: estimate.estimate_source,
|
|
148
|
+
confidence: estimate.confidence ?? null,
|
|
149
|
+
estimated_lane: estimate.lane,
|
|
150
|
+
terminal_status: actual?.terminal_status ?? null,
|
|
151
|
+
realized_lane: lane,
|
|
152
|
+
matched: eligible ? estimate.lane === lane : false,
|
|
153
|
+
files_changed_count: actual?.files_changed_count ?? null,
|
|
154
|
+
files_source: actual?.files_source ?? null,
|
|
155
|
+
review_iterations: actual?.review_iterations ?? null,
|
|
156
|
+
escalations: actual?.escalations ?? null,
|
|
157
|
+
};
|
|
158
|
+
})
|
|
159
|
+
.sort((a, b) => a.build_id.localeCompare(b.build_id));
|
|
160
|
+
const eligibleRows = acrrRows.filter((row) => row.terminal_status === 'complete' && row.estimate_source !== 'escalated');
|
|
161
|
+
const escalatedRows = acrrRows.filter((row) => row.terminal_status === 'complete' && row.estimate_source === 'escalated');
|
|
162
|
+
const failed_attempts = actualRows.filter((row) => row.terminal_status === 'failed').length;
|
|
163
|
+
const aborted_attempts = actualRows.filter((row) => row.terminal_status === 'aborted').length;
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
v: 1,
|
|
167
|
+
filters: { since: since ?? null, feature: feature ?? null },
|
|
168
|
+
coverage: {
|
|
169
|
+
dispatch_count: dispatches.length,
|
|
170
|
+
unattributed_count: dispatches.filter((row) => row.site === 'unattributed').length,
|
|
171
|
+
null_usage_count: dispatches.filter((row) => !isNumber(row.tokens_total)).length,
|
|
172
|
+
},
|
|
173
|
+
model_effort,
|
|
174
|
+
sites,
|
|
175
|
+
acrr: {
|
|
176
|
+
eligible: acrrSummary(eligibleRows),
|
|
177
|
+
escalated: acrrSummary(escalatedRows),
|
|
178
|
+
attrition_count: failed_attempts + aborted_attempts,
|
|
179
|
+
failed_attempts,
|
|
180
|
+
aborted_attempts,
|
|
181
|
+
pending_count: acrrRows.filter((row) => row.terminal_status === null).length,
|
|
182
|
+
unpaired_count: [...actuals.keys()].filter((buildId) => !estimateBuildIds.has(buildId)).length,
|
|
183
|
+
rows: acrrRows,
|
|
184
|
+
},
|
|
185
|
+
known_limitations: { retry_rate: RETRY_NOTE, gsd_acceptance: GSD_NOTE },
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function formatRate(value) {
|
|
190
|
+
return value === null ? 'n/a' : `${(value * 100).toFixed(1)}%`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function formatMedian(value) {
|
|
194
|
+
return value === null ? 'n/a' : String(value);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function formatAcceptance(acceptance) {
|
|
198
|
+
return acceptance.note === GSD_NOTE ? 'n/a (not instrumented)' : formatRate(acceptance.rate);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Render a compact, stable human report for `compose metrics`. */
|
|
202
|
+
export function renderDispatchMetrics(report) {
|
|
203
|
+
const lines = [
|
|
204
|
+
'Model × executed effort',
|
|
205
|
+
'model | executed | intended when unknown | dispatches | median tokens | median ms | completion | acceptance | retry | usd',
|
|
206
|
+
];
|
|
207
|
+
for (const row of report.model_effort) {
|
|
208
|
+
lines.push([
|
|
209
|
+
row.model === null ? '(model: unknown)' : row.model,
|
|
210
|
+
row.effort_executed ?? 'unknown',
|
|
211
|
+
row.effort_intended_when_unknown ?? '—',
|
|
212
|
+
row.dispatch_count,
|
|
213
|
+
formatMedian(row.median_tokens),
|
|
214
|
+
formatMedian(row.median_duration_ms),
|
|
215
|
+
formatRate(row.completion.rate),
|
|
216
|
+
formatAcceptance(row.acceptance),
|
|
217
|
+
formatRate(row.retry.rate),
|
|
218
|
+
row.usd_total,
|
|
219
|
+
].join(' | '));
|
|
220
|
+
}
|
|
221
|
+
lines.push('', 'Sites', 'site | dispatches | median tokens | median ms | completion | acceptance | retry | usd');
|
|
222
|
+
for (const row of report.sites) {
|
|
223
|
+
lines.push([
|
|
224
|
+
row.site, row.dispatch_count, formatMedian(row.median_tokens), formatMedian(row.median_duration_ms),
|
|
225
|
+
formatRate(row.completion.rate), formatAcceptance(row.acceptance), formatRate(row.retry.rate), row.usd_total,
|
|
226
|
+
].join(' | '));
|
|
227
|
+
}
|
|
228
|
+
lines.push('', 'ACRR');
|
|
229
|
+
lines.push(`eligible: ${report.acrr.eligible.matched}/${report.acrr.eligible.total} (${formatRate(report.acrr.eligible.rate)})`);
|
|
230
|
+
lines.push(`escalated: ${report.acrr.escalated.matched}/${report.acrr.escalated.total} (${formatRate(report.acrr.escalated.rate)})`);
|
|
231
|
+
lines.push(`attrition: ${report.acrr.attrition_count} (failed attempts: ${report.acrr.failed_attempts}, aborted attempts: ${report.acrr.aborted_attempts}, pending: ${report.acrr.pending_count}, unpaired: ${report.acrr.unpaired_count})`);
|
|
232
|
+
lines.push('', 'Known limitations');
|
|
233
|
+
lines.push(`retry rate: ${report.known_limitations.retry_rate}`);
|
|
234
|
+
lines.push(`GSD acceptance: ${report.known_limitations.gsd_acceptance}`);
|
|
235
|
+
return `${lines.join('\n')}\n`;
|
|
236
|
+
}
|
package/lib/experiment-judge.js
CHANGED
|
@@ -121,6 +121,7 @@ export async function judge({ diff, goal, judgeModel, stratum, cwd }) {
|
|
|
121
121
|
try {
|
|
122
122
|
const prompt = buildJudgePrompt(diff, goal);
|
|
123
123
|
const { provider, modelID, thinking, effort } = resolveAgentConfig(judgeModel);
|
|
124
|
+
const telemetry = { site: 'judge', project_cwd: cwd };
|
|
124
125
|
|
|
125
126
|
let text;
|
|
126
127
|
if (typeof stratum.agentRun === 'function') {
|
|
@@ -130,11 +131,15 @@ export async function judge({ diff, goal, judgeModel, stratum, cwd }) {
|
|
|
130
131
|
thinking: thinking ?? undefined,
|
|
131
132
|
effort: effort ?? undefined,
|
|
132
133
|
cwd: cwd ?? undefined,
|
|
134
|
+
telemetry,
|
|
133
135
|
});
|
|
134
136
|
text = result?.text ?? '';
|
|
135
137
|
} else {
|
|
136
138
|
// Fallback: runAgentText (no model pinning — test harnesses may use this).
|
|
137
|
-
text = await stratum.runAgentText(provider, prompt, {
|
|
139
|
+
text = await stratum.runAgentText(provider, prompt, {
|
|
140
|
+
cwd: cwd ?? undefined,
|
|
141
|
+
telemetry,
|
|
142
|
+
});
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
return extractJudgeResult(text);
|
package/lib/feature-json.js
CHANGED
|
@@ -18,7 +18,7 @@ import { resolvePathValue } from './paths-core.js';
|
|
|
18
18
|
* Identical to join(cwd, featuresDir) for plain relative dirs.
|
|
19
19
|
* @returns {string} absolute features dir
|
|
20
20
|
*/
|
|
21
|
-
function featuresBase(cwd, featuresDir) {
|
|
21
|
+
export function featuresBase(cwd, featuresDir) {
|
|
22
22
|
return resolvePathValue(cwd, featuresDir, 'features');
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -239,6 +239,14 @@ async function applyEntry(cwd, entry) {
|
|
|
239
239
|
const writer = new VisionWriter(join(cwd, '.compose', 'data'));
|
|
240
240
|
const item = await writer.findFeatureItem(entry.feature_code);
|
|
241
241
|
if (!item) return { changed: false };
|
|
242
|
+
if (entry._visStatus === 'complete') {
|
|
243
|
+
// COMP-COMPLETION-GATE slice 3 (AC-16a): repair is a projection of
|
|
244
|
+
// canonical truth, so it routes through the self-verifying seam rather
|
|
245
|
+
// than a carve-out. completeItem re-reads feature.json (and the guard
|
|
246
|
+
// ledger when a resource exists) before writing, and stamps the tier.
|
|
247
|
+
await writer.completeItem(item.id, { featureCode: entry.feature_code, cwd });
|
|
248
|
+
return { changed: true };
|
|
249
|
+
}
|
|
242
250
|
await writer.updateItemStatus(item.id, entry._visStatus);
|
|
243
251
|
return { changed: true };
|
|
244
252
|
}
|
package/lib/feature-validator.js
CHANGED
|
@@ -24,6 +24,16 @@
|
|
|
24
24
|
* - XREF_URL_UNCHECKED (info) url + reserved url-class providers
|
|
25
25
|
* (jira|linear|notion|obsidian) — recorded,
|
|
26
26
|
* not resolved
|
|
27
|
+
* Plus the 5 COMP-COVERAGE-GATE project-scope kinds, which describe Compose's
|
|
28
|
+
* OWN MCP tool surface rather than the workspace (see lib/coverage-gate.js):
|
|
29
|
+
* - MISSING_EFFECT (error) tool definition declares no `effect`
|
|
30
|
+
* - UNGATED_MUTATION (warning) mutating tool named by no profile list
|
|
31
|
+
* - ORPHAN_REGISTRY_TOOL (warning) canon entry names a tool that is gone
|
|
32
|
+
* - UNCOVERED_WRITE (info) declared canon write absent from that
|
|
33
|
+
* entry's tool list (deny-message honesty)
|
|
34
|
+
* - COVERAGE_CHECK_SKIPPED (warning) the check itself errored — never aborts
|
|
35
|
+
* These appear in `findings` AND, structured, under `result.coverage`.
|
|
36
|
+
*
|
|
27
37
|
* Full catalog + trigger/degrade/gating contract:
|
|
28
38
|
* docs/features/COMP-MCP-VALIDATE/design.md
|
|
29
39
|
*/
|
|
@@ -46,6 +56,14 @@ import { SchemaValidator } from '../server/schema-validator.js';
|
|
|
46
56
|
import {
|
|
47
57
|
resolveRoadmapPathFromConfig, resolveFeaturesPathFromConfig, resolveJournalPathFromConfig,
|
|
48
58
|
} from './project-paths.js';
|
|
59
|
+
// COMP-COVERAGE-GATE slice 2. Imported from server/mcp-tool-defs.js, NEVER from
|
|
60
|
+
// server/compose-mcp.js — the latter connects a StdioServerTransport at module
|
|
61
|
+
// load and hangs any importer.
|
|
62
|
+
import { TOOLS as MCP_TOOL_DEFS } from '../server/mcp-tool-defs.js';
|
|
63
|
+
import { loadToolInventory } from './tool-inventory.js';
|
|
64
|
+
import { canonEntries } from './canon-registry.js';
|
|
65
|
+
import { PROFILE_POLICY, PHASE_REFINEMENT } from '../server/mcp-tool-policy.js';
|
|
66
|
+
import { checkAuthorizationCoverage } from './coverage-gate.js';
|
|
49
67
|
|
|
50
68
|
const __filename = fileURLToPath(import.meta.url);
|
|
51
69
|
const __dirname = path.dirname(__filename);
|
|
@@ -1130,6 +1148,44 @@ async function runExternalRefChecks(ctx, findings, options = {}) {
|
|
|
1130
1148
|
}
|
|
1131
1149
|
}
|
|
1132
1150
|
|
|
1151
|
+
/**
|
|
1152
|
+
* COMP-COVERAGE-GATE slice 2 — authorization coverage of Compose's OWN MCP tool
|
|
1153
|
+
* surface.
|
|
1154
|
+
*
|
|
1155
|
+
* Project-scoped, not feature-scoped: it is a property of the installation, not
|
|
1156
|
+
* of any one feature, so it is deliberately absent from validateFeature.
|
|
1157
|
+
*
|
|
1158
|
+
* Pushes its findings into the main `findings` array (so the CLI exit code,
|
|
1159
|
+
* --block-on and the REST severity rollup all pick them up with no fork) AND
|
|
1160
|
+
* returns the structured section for callers that want the codes and
|
|
1161
|
+
* remediations without re-parsing prose.
|
|
1162
|
+
*
|
|
1163
|
+
* Never throws: a coverage bug must not take down a validate run that is mostly
|
|
1164
|
+
* about the workspace.
|
|
1165
|
+
*
|
|
1166
|
+
* @param {Array} findings — mutated in place
|
|
1167
|
+
* @returns {{ findings: Array }}
|
|
1168
|
+
*/
|
|
1169
|
+
function runCoverageCheck(findings) {
|
|
1170
|
+
try {
|
|
1171
|
+
const inventory = loadToolInventory(MCP_TOOL_DEFS);
|
|
1172
|
+
const result = checkAuthorizationCoverage({
|
|
1173
|
+
inventory,
|
|
1174
|
+
registry: canonEntries(),
|
|
1175
|
+
policy: { PROFILE_POLICY, PHASE_REFINEMENT },
|
|
1176
|
+
});
|
|
1177
|
+
for (const f of result.findings) {
|
|
1178
|
+
findings.push(finding(f.severity, f.code, null, f.remediation, 'coverage'));
|
|
1179
|
+
}
|
|
1180
|
+
return result;
|
|
1181
|
+
} catch (e) {
|
|
1182
|
+
findings.push(finding('warning', 'COVERAGE_CHECK_SKIPPED', null,
|
|
1183
|
+
`authorization coverage check skipped (unexpected error): ${e && e.message ? e.message : e}`,
|
|
1184
|
+
'coverage'));
|
|
1185
|
+
return { findings: [] };
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1133
1189
|
export async function validateProject(cwd, options = {}) {
|
|
1134
1190
|
const ctx = loadValidationContext(cwd, options);
|
|
1135
1191
|
const findings = [];
|
|
@@ -1234,5 +1290,12 @@ export async function validateProject(cwd, options = {}) {
|
|
|
1234
1290
|
// false positive. Strip them in one place (robust against new such checks) and
|
|
1235
1291
|
// record the skip as a single info finding. feature.json↔vision drift is left
|
|
1236
1292
|
// intact — it doesn't involve the roadmap.
|
|
1237
|
-
|
|
1293
|
+
const coverage = runCoverageCheck(findings);
|
|
1294
|
+
|
|
1295
|
+
return {
|
|
1296
|
+
scope: 'project',
|
|
1297
|
+
validated_at: nowIso(),
|
|
1298
|
+
findings: applyNarrativeSuppression(findings, ctx),
|
|
1299
|
+
coverage,
|
|
1300
|
+
};
|
|
1238
1301
|
}
|