@smartmemory/compose 0.1.0 → 0.1.2-beta

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.
Files changed (124) hide show
  1. package/.claude/skills/bug-fix/SKILL.md +143 -0
  2. package/.claude/skills/compose/SKILL.md +604 -0
  3. package/.compose-deps.json +89 -0
  4. package/README.md +14 -3
  5. package/bin/compose.js +473 -0
  6. package/contracts/comp-obs-contract.schema.json +362 -0
  7. package/contracts/cross-model-review-result.json +78 -0
  8. package/contracts/review-result.json +126 -0
  9. package/dist/assets/{_baseUniq-CQwX6VLz.js → _baseUniq-D-avYfn5.js} +1 -1
  10. package/dist/assets/{arc-SxJ2J1sh.js → arc-BC4dfQ-X.js} +1 -1
  11. package/dist/assets/{architectureDiagram-Q4EWVU46-BykunY1F.js → architectureDiagram-Q4EWVU46-BZmFXnGI.js} +1 -1
  12. package/dist/assets/{blockDiagram-DXYQGD6D-ohAKBOUw.js → blockDiagram-DXYQGD6D-DlfWSuux.js} +1 -1
  13. package/dist/assets/{c4Diagram-AHTNJAMY-DBDC3ENB.js → c4Diagram-AHTNJAMY-Y__uJrRx.js} +1 -1
  14. package/dist/assets/channel-LRG9kHqJ.js +1 -0
  15. package/dist/assets/{chunk-4BX2VUAB-Cv93Z7uM.js → chunk-4BX2VUAB-BfMePfTp.js} +1 -1
  16. package/dist/assets/{chunk-4TB4RGXK-DE0WBDkj.js → chunk-4TB4RGXK-BdlMSdEA.js} +1 -1
  17. package/dist/assets/{chunk-55IACEB6-CE1EXenG.js → chunk-55IACEB6-vrQHZTdv.js} +1 -1
  18. package/dist/assets/{chunk-EDXVE4YY-DA7Ana6H.js → chunk-EDXVE4YY-B8wioVlW.js} +1 -1
  19. package/dist/assets/{chunk-FMBD7UC4-CTDIPA3p.js → chunk-FMBD7UC4-Cd6Hrux2.js} +1 -1
  20. package/dist/assets/{chunk-OYMX7WX6-uGBaPaTX.js → chunk-OYMX7WX6-CfrhdQXY.js} +1 -1
  21. package/dist/assets/{chunk-QZHKN3VN-CYlnXuUO.js → chunk-QZHKN3VN-B9JQerOU.js} +1 -1
  22. package/dist/assets/{chunk-YZCP3GAM-ojGkzcZK.js → chunk-YZCP3GAM-DFN9X99H.js} +1 -1
  23. package/dist/assets/classDiagram-6PBFFD2Q-BC9a6pDE.js +1 -0
  24. package/dist/assets/classDiagram-v2-HSJHXN6E-BC9a6pDE.js +1 -0
  25. package/dist/assets/clone-dRxgFrBv.js +1 -0
  26. package/dist/assets/{cose-bilkent-S5V4N54A-Bktn9hL-.js → cose-bilkent-S5V4N54A-BAn0ap_E.js} +1 -1
  27. package/dist/assets/{dagre-KV5264BT-DFaSzuRF.js → dagre-KV5264BT-DyxnVq1g.js} +1 -1
  28. package/dist/assets/{diagram-5BDNPKRD-DnfmDzEm.js → diagram-5BDNPKRD-XCrzqski.js} +1 -1
  29. package/dist/assets/{diagram-G4DWMVQ6-Bm8W9YnG.js → diagram-G4DWMVQ6-MBCAXft_.js} +1 -1
  30. package/dist/assets/{diagram-MMDJMWI5-B5-TSKvp.js → diagram-MMDJMWI5-DbtB2yS6.js} +1 -1
  31. package/dist/assets/{diagram-TYMM5635-ls4rqlky.js → diagram-TYMM5635-Bb5NzX61.js} +1 -1
  32. package/dist/assets/{erDiagram-SMLLAGMA-giG6WO-r.js → erDiagram-SMLLAGMA-CpIeCOh2.js} +1 -1
  33. package/dist/assets/{flowDiagram-DWJPFMVM-XvlUuz-7.js → flowDiagram-DWJPFMVM-CHyoKnhW.js} +1 -1
  34. package/dist/assets/{ganttDiagram-T4ZO3ILL-hLBV57oV.js → ganttDiagram-T4ZO3ILL-DErKteO_.js} +1 -1
  35. package/dist/assets/{gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js → gitGraphDiagram-UUTBAWPF-KFVAtj2F.js} +1 -1
  36. package/dist/assets/{graph-D0Cfv00Y.js → graph-CRnO_ifT.js} +1 -1
  37. package/dist/assets/index-DKBsEUJ-.css +1 -0
  38. package/dist/assets/index-DkRKLuNr.js +1144 -0
  39. package/dist/assets/{infoDiagram-42DDH7IO-DbqRsOo3.js → infoDiagram-42DDH7IO-BZFnuSp5.js} +1 -1
  40. package/dist/assets/{ishikawaDiagram-UXIWVN3A-DnCdx7zb.js → ishikawaDiagram-UXIWVN3A-4Xe2Szde.js} +1 -1
  41. package/dist/assets/{journeyDiagram-VCZTEJTY-CfD7eNcP.js → journeyDiagram-VCZTEJTY-CZRByfS-.js} +1 -1
  42. package/dist/assets/{kanban-definition-6JOO6SKY-BYaO9-mK.js → kanban-definition-6JOO6SKY-B95sk6Fk.js} +1 -1
  43. package/dist/assets/{layout-Bj72wOEB.js → layout-BqNQzxWT.js} +1 -1
  44. package/dist/assets/{linear-BRFo114D.js → linear-CUh7qb64.js} +1 -1
  45. package/dist/assets/{min-GCHnKlJS.js → min-wXgOS3ig.js} +1 -1
  46. package/dist/assets/{mindmap-definition-QFDTVHPH-n0PMebY4.js → mindmap-definition-QFDTVHPH-DB6iaAbO.js} +1 -1
  47. package/dist/assets/{pieDiagram-DEJITSTG-pN4CljHF.js → pieDiagram-DEJITSTG-CHkZHrTW.js} +1 -1
  48. package/dist/assets/{quadrantDiagram-34T5L4WZ-DNoAy8-D.js → quadrantDiagram-34T5L4WZ-DoTEO8e3.js} +1 -1
  49. package/dist/assets/{requirementDiagram-MS252O5E-BhtY05PT.js → requirementDiagram-MS252O5E-Dn8peXYp.js} +1 -1
  50. package/dist/assets/{sankeyDiagram-XADWPNL6-B6AD-16A.js → sankeyDiagram-XADWPNL6-DRXs6Ipb.js} +1 -1
  51. package/dist/assets/{sequenceDiagram-FGHM5R23-DShHM-uk.js → sequenceDiagram-FGHM5R23-wBBYZ0aq.js} +1 -1
  52. package/dist/assets/{stateDiagram-FHFEXIEX-DMxn7HTo.js → stateDiagram-FHFEXIEX-DPlBNGmf.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-QKLJ7IA2-BW0ezXb4.js +1 -0
  54. package/dist/assets/{timeline-definition-GMOUNBTQ-Cdu6uq52.js → timeline-definition-GMOUNBTQ-CbbyTlHk.js} +1 -1
  55. package/dist/assets/{vennDiagram-DHZGUBPP-CpK29iRe.js → vennDiagram-DHZGUBPP-Bj4GaFfj.js} +1 -1
  56. package/dist/assets/{wardley-RL74JXVD-BQgSkdcO.js → wardley-RL74JXVD-RtNzq8KU.js} +55 -55
  57. package/dist/assets/{wardleyDiagram-NUSXRM2D-DJHYev6O.js → wardleyDiagram-NUSXRM2D-CDfE3zSj.js} +1 -1
  58. package/dist/assets/{xychartDiagram-5P7HB3ND-1d75pbaO.js → xychartDiagram-5P7HB3ND-CZXHHYD5.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/lib/budget-ledger.js +45 -0
  61. package/lib/bug-bisect.js +292 -0
  62. package/lib/bug-checkpoint.js +191 -0
  63. package/lib/bug-escalation.js +306 -0
  64. package/lib/bug-index-gen.js +136 -0
  65. package/lib/bug-ledger.js +126 -0
  66. package/lib/build-stream-schema.js +176 -0
  67. package/lib/build-stream-writer.js +3 -1
  68. package/lib/build.js +854 -284
  69. package/lib/connector-factory-shim.js +167 -0
  70. package/lib/constants.js +18 -0
  71. package/lib/debug-discipline.js +176 -27
  72. package/lib/deps.js +205 -0
  73. package/lib/health-score.js +4 -4
  74. package/lib/import.js +26 -13
  75. package/lib/inject-schema.js +21 -0
  76. package/lib/new.js +27 -53
  77. package/lib/result-normalizer.js +160 -144
  78. package/lib/review-lenses.js +5 -5
  79. package/lib/review-normalize.js +413 -0
  80. package/lib/review-prompt.js +163 -0
  81. package/lib/sections.js +325 -0
  82. package/lib/step-prompt.js +21 -1
  83. package/lib/step-validator.js +5 -3
  84. package/lib/stratum-mcp-client.js +172 -7
  85. package/package.json +14 -3
  86. package/pipelines/bug-fix.stratum.yaml +39 -1
  87. package/pipelines/build.stratum.yaml +28 -45
  88. package/pipelines/review-fix.stratum.yaml +1 -1
  89. package/presets/team-review.stratum.yaml +21 -14
  90. package/server/build-stream-bridge.js +28 -0
  91. package/server/cc-session-feature-resolver.js +111 -0
  92. package/server/cc-session-reader.js +327 -0
  93. package/server/cc-session-watcher.js +318 -0
  94. package/server/compose-mcp-tools.js +0 -125
  95. package/server/compose-mcp.js +2 -4
  96. package/server/contract-diff.js +192 -0
  97. package/server/decision-event-emit.js +175 -0
  98. package/server/decision-event-id.js +64 -0
  99. package/server/decision-events-snapshot.js +166 -0
  100. package/server/design-routes.js +92 -49
  101. package/server/drift-axes.js +365 -0
  102. package/server/drift-emit.js +121 -0
  103. package/server/gate-log-store.js +102 -0
  104. package/server/lifecycle-phase-history.js +44 -0
  105. package/server/open-loops-store.js +102 -0
  106. package/server/schema-validator.js +49 -0
  107. package/server/status-emit.js +27 -0
  108. package/server/status-snapshot.js +218 -0
  109. package/server/vision-routes.js +332 -4
  110. package/server/vision-server.js +104 -12
  111. package/server/vision-store.js +21 -0
  112. package/dist/assets/channel-DGElom1e.js +0 -1
  113. package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +0 -1
  114. package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +0 -1
  115. package/dist/assets/clone-DUJKJXd7.js +0 -1
  116. package/dist/assets/index-CUd6pFGF.css +0 -1
  117. package/dist/assets/index-DReRlzZI.js +0 -1144
  118. package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +0 -1
  119. package/server/connectors/agent-connector.js +0 -78
  120. package/server/connectors/claude-sdk-connector.js +0 -198
  121. package/server/connectors/codex-connector.js +0 -240
  122. package/server/connectors/connector-discovery.js +0 -18
  123. package/server/connectors/connector-runtime.js +0 -13
  124. package/server/connectors/opencode-connector.js +0 -200
@@ -10,8 +10,6 @@ import http from 'node:http';
10
10
  import path from 'node:path';
11
11
  import { ArtifactManager, ARTIFACT_SCHEMAS } from './artifact-manager.js';
12
12
  import { getTargetRoot, getDataDir, resolveProjectPath } from './project-root.js';
13
- import { ClaudeSDKConnector } from './connectors/claude-sdk-connector.js';
14
- import { CodexConnector } from './connectors/codex-connector.js';
15
13
 
16
14
  export const PROJECT_ROOT = getTargetRoot();
17
15
  export const VISION_FILE = path.join(getDataDir(), 'vision-state.json');
@@ -354,126 +352,3 @@ export function toolGetPendingGates({ itemId }) {
354
352
  return { count: pending.length, gates: pending };
355
353
  }
356
354
 
357
- // ---------------------------------------------------------------------------
358
- // Agent run — dispatch prompts to claude or codex
359
- // ---------------------------------------------------------------------------
360
-
361
- const VALID_AGENT_TYPES = new Set(['claude', 'codex']);
362
-
363
- /**
364
- * Build a context preamble for agent_run prompts.
365
- * The spawned agent (especially codex via opencode run) has no project context —
366
- * no CLAUDE.md, no feature folder awareness, no compose/stratum semantics.
367
- * This function reads project files and prepends them so the agent can do useful work.
368
- */
369
- function _buildContext({ featureCode }) {
370
- const sections = [];
371
-
372
- // Project instructions
373
- const claudeMd = path.join(PROJECT_ROOT, 'CLAUDE.md');
374
- if (fs.existsSync(claudeMd)) {
375
- try {
376
- const content = fs.readFileSync(claudeMd, 'utf-8');
377
- sections.push(`## Project Instructions (CLAUDE.md)\n\n${content}`);
378
- } catch { /* ignore read errors */ }
379
- }
380
-
381
- // Feature artifacts (if feature code detected)
382
- // Cap total feature context to ~20KB to avoid exceeding model input limits.
383
- // Prioritize design.md first, then most recent files by mtime.
384
- const MAX_FEATURE_BYTES = 20_000;
385
- if (featureCode) {
386
- const featureRoot = resolveProjectPath('features');
387
- const featureDir = path.join(featureRoot, featureCode);
388
- if (fs.existsSync(featureDir)) {
389
- const artifacts = [];
390
- let totalBytes = 0;
391
- try {
392
- const files = fs.readdirSync(featureDir)
393
- .filter(f => f.endsWith('.md') || f.endsWith('.json'))
394
- .map(f => ({ name: f, path: path.join(featureDir, f), stat: fs.statSync(path.join(featureDir, f)) }))
395
- .filter(f => f.stat.isFile())
396
- // design.md first, then most recently modified
397
- .sort((a, b) => {
398
- if (a.name === 'design.md') return -1;
399
- if (b.name === 'design.md') return 1;
400
- return b.stat.mtimeMs - a.stat.mtimeMs;
401
- });
402
- for (const file of files) {
403
- if (totalBytes >= MAX_FEATURE_BYTES) break;
404
- try {
405
- const content = fs.readFileSync(file.path, 'utf-8');
406
- const trimmed = content.slice(0, MAX_FEATURE_BYTES - totalBytes);
407
- artifacts.push(`### ${file.name}\n\n${trimmed}`);
408
- totalBytes += trimmed.length;
409
- } catch { /* skip unreadable files */ }
410
- }
411
- } catch { /* ignore readdir errors */ }
412
- if (artifacts.length > 0) {
413
- sections.push(`## Feature: ${featureCode}\n\n${artifacts.join('\n\n---\n\n')}`);
414
- }
415
- }
416
- }
417
-
418
- if (sections.length === 0) return '';
419
- return `# Context\n\n${sections.join('\n\n---\n\n')}\n\n---\n\n`;
420
- }
421
-
422
- /**
423
- * Extract a feature code from the prompt if one is referenced.
424
- * Looks for common patterns like "FEAT-1", "AUTH-2", or feature folder paths.
425
- */
426
- function _extractFeatureCode(prompt) {
427
- // Match uppercase CODE-N patterns (e.g. FEAT-1, AUTH-2, STRAT-COMP-3)
428
- const codeMatch = prompt.match(/\b([A-Z][\w-]*-\d+)\b/);
429
- if (codeMatch) return codeMatch[1];
430
-
431
- // Match feature folder references
432
- const pathMatch = prompt.match(/features\/([a-zA-Z][\w-]+)/);
433
- if (pathMatch) return pathMatch[1];
434
-
435
- return null;
436
- }
437
-
438
- export async function toolAgentRun({ type = 'claude', prompt, schema, modelID, cwd, featureCode }) {
439
- if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
440
- throw new Error('agent_run: prompt is required');
441
- }
442
- if (!VALID_AGENT_TYPES.has(type)) {
443
- throw new Error(`agent_run: unknown type '${type}'. Valid: ${[...VALID_AGENT_TYPES].join(', ')}`);
444
- }
445
-
446
- // Resolve feature code from explicit param or prompt text
447
- const resolvedFeature = featureCode || _extractFeatureCode(prompt);
448
-
449
- // Build context preamble and prepend to prompt
450
- const context = _buildContext({ featureCode: resolvedFeature });
451
- const fullPrompt = context ? `${context}# Task\n\n${prompt}` : prompt;
452
-
453
- const resolvedCwd = cwd || PROJECT_ROOT;
454
- const connector = type === 'codex'
455
- ? new CodexConnector({ modelID, cwd: resolvedCwd })
456
- : new ClaudeSDKConnector({ model: modelID, cwd: resolvedCwd });
457
-
458
- const parts = [];
459
- for await (const event of connector.run(fullPrompt, { schema, modelID, cwd: resolvedCwd })) {
460
- if (event.type === 'assistant' && event.content) {
461
- parts.push(event.content);
462
- } else if (event.type === 'error') {
463
- throw new Error(`agent_run (${type}): ${event.message}`);
464
- }
465
- }
466
-
467
- const text = parts.join('');
468
-
469
- if (schema) {
470
- try {
471
- return { text, result: JSON.parse(text) };
472
- } catch {
473
- return { text, result: null, parseError: 'Response was not valid JSON' };
474
- }
475
- }
476
-
477
- return { text };
478
- }
479
-
@@ -256,10 +256,8 @@ const TOOLS = [
256
256
  },
257
257
  },
258
258
  },
259
- // Note: `agent_run` tool removed 2026-04-18 (STRAT-DEDUP-AGENTRUN v1).
260
- // LLM-facing agent dispatch now goes through `mcp__stratum__stratum_agent_run`.
261
- // `toolAgentRun` (and the Node connectors it uses) remain exported for Compose's
262
- // internal callers (build.js, vision-server, pipelines).
259
+ // `agent_run` tool removed 2026-04-18 (STRAT-DEDUP-AGENTRUN v1); LLM-facing
260
+ // dispatch goes through `mcp__stratum__stratum_agent_run`.
263
261
  ];
264
262
 
265
263
  // ---------------------------------------------------------------------------
@@ -0,0 +1,192 @@
1
+ /**
2
+ * contract-diff.js — COMP-OBS-DRIFT contract_drift axis helper.
3
+ *
4
+ * Compares JSON Schema files between an anchor git ref and the current working
5
+ * tree. Returns field-change counts (added / removed / retyped / total) for use
6
+ * by drift-axes.js in computing the contract_drift ratio.
7
+ *
8
+ * Field-walk strategy: recursively collect leaf property names from
9
+ * schema.properties, counting each as one "field". additionalProperties: false
10
+ * itself counts as a single field (stability signal). Combines all files so
11
+ * the denominator is global-per-feature, not per-file.
12
+ *
13
+ * All git invocations go through execSync; errors are caught and returned as
14
+ * empty field sets (axis falls back to threshold: null in the caller).
15
+ */
16
+
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { execSync } from 'node:child_process';
20
+
21
+ /**
22
+ * Walk a JSON Schema object and return the set of "fields" it declares.
23
+ * Each property name is included; additionalProperties:false adds a sentinel
24
+ * '__additionalProperties_closed__' so it contributes to the count.
25
+ *
26
+ * @param {object} schema
27
+ * @param {string} [prefix]
28
+ * @returns {Set<string>}
29
+ */
30
+ function walkSchema(schema, prefix = '') {
31
+ const fields = new Set();
32
+ if (!schema || typeof schema !== 'object') return fields;
33
+
34
+ if (schema.additionalProperties === false) {
35
+ fields.add(`${prefix}__additionalProperties_closed__`);
36
+ }
37
+
38
+ const props = schema.properties;
39
+ if (props && typeof props === 'object') {
40
+ for (const [key, value] of Object.entries(props)) {
41
+ const qualifiedKey = prefix ? `${prefix}.${key}` : key;
42
+ fields.add(qualifiedKey);
43
+ // Recurse into nested object schemas
44
+ if (value && typeof value === 'object') {
45
+ const nested = walkSchema(value, qualifiedKey);
46
+ for (const f of nested) fields.add(f);
47
+ }
48
+ }
49
+ }
50
+
51
+ // Walk allOf / anyOf / oneOf for completeness
52
+ for (const key of ['allOf', 'anyOf', 'oneOf']) {
53
+ if (Array.isArray(schema[key])) {
54
+ for (const sub of schema[key]) {
55
+ const nested = walkSchema(sub, prefix);
56
+ for (const f of nested) fields.add(f);
57
+ }
58
+ }
59
+ }
60
+
61
+ return fields;
62
+ }
63
+
64
+ /**
65
+ * Collect a Map<fullyQualifiedPath, typeString> for every field at any depth.
66
+ * Mirrors walkSchema's traversal so paths are comparable across versions.
67
+ *
68
+ * @param {object} schema
69
+ * @param {string} prefix
70
+ * @returns {Map<string, string>}
71
+ */
72
+ function collectFieldTypes(schema, prefix = '') {
73
+ const types = new Map();
74
+ if (!schema || typeof schema !== 'object') return types;
75
+
76
+ if (schema.properties && typeof schema.properties === 'object') {
77
+ for (const [key, value] of Object.entries(schema.properties)) {
78
+ const qualifiedKey = prefix ? `${prefix}.${key}` : key;
79
+ const t = JSON.stringify(value?.type ?? null);
80
+ types.set(qualifiedKey, t);
81
+ if (value && typeof value === 'object') {
82
+ const nested = collectFieldTypes(value, qualifiedKey);
83
+ for (const [k, v] of nested) types.set(k, v);
84
+ }
85
+ }
86
+ }
87
+
88
+ for (const key of ['allOf', 'anyOf', 'oneOf']) {
89
+ if (Array.isArray(schema[key])) {
90
+ for (const sub of schema[key]) {
91
+ const nested = collectFieldTypes(sub, prefix);
92
+ for (const [k, v] of nested) types.set(k, v);
93
+ }
94
+ }
95
+ }
96
+
97
+ return types;
98
+ }
99
+
100
+ /**
101
+ * Read a file at an anchor git ref via `git show <ref>:<path>`.
102
+ * Returns null if the file did not exist at that ref.
103
+ *
104
+ * @param {string} ref — git commit ref (sha1 or symbolic)
105
+ * @param {string} filePath — path relative to repo root
106
+ * @param {string} projectRoot — absolute path of the git repo root
107
+ * @returns {string|null}
108
+ */
109
+ function gitShow(ref, filePath, projectRoot) {
110
+ // Normalize path separators to forward slashes for git
111
+ const gitPath = filePath.replace(/\\/g, '/');
112
+ try {
113
+ return execSync(`git show ${ref}:${gitPath}`, {
114
+ cwd: projectRoot,
115
+ encoding: 'utf8',
116
+ stdio: ['pipe', 'pipe', 'pipe'],
117
+ });
118
+ } catch {
119
+ return null; // file didn't exist at that ref — treat as empty schema
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Diff contract files for a feature between an anchor ref and the current
125
+ * working tree.
126
+ *
127
+ * @param {string} anchorRef — git commit ref (result of git rev-list)
128
+ * @param {string[]} headPaths — absolute paths to current JSON schema files
129
+ * @param {string} projectRoot — absolute root of the git repo
130
+ * @returns {{ added: number, removed: number, retyped: number, total: number }}
131
+ * Returns { added:0, removed:0, retyped:0, total:0 } on any parse failure.
132
+ */
133
+ export function diffContracts(anchorRef, headPaths, projectRoot) {
134
+ let added = 0;
135
+ let removed = 0;
136
+ let retyped = 0;
137
+ let totalCurrentFields = 0;
138
+
139
+ for (const absPath of headPaths) {
140
+ // Read current (HEAD working-tree) version
141
+ let currentSchema;
142
+ try {
143
+ currentSchema = JSON.parse(fs.readFileSync(absPath, 'utf8'));
144
+ } catch {
145
+ continue; // unparseable — skip this file
146
+ }
147
+
148
+ // Compute path relative to projectRoot for git show
149
+ const relPath = path.relative(projectRoot, absPath);
150
+
151
+ // Read anchor version
152
+ const anchorContent = gitShow(anchorRef, relPath, projectRoot);
153
+ let anchorSchema = null;
154
+ if (anchorContent) {
155
+ try {
156
+ anchorSchema = JSON.parse(anchorContent);
157
+ } catch {
158
+ // anchor version unparseable — treat as if it didn't exist
159
+ }
160
+ }
161
+
162
+ const currentFields = walkSchema(currentSchema);
163
+ const anchorFields = anchorSchema ? walkSchema(anchorSchema) : new Set();
164
+
165
+ totalCurrentFields += currentFields.size;
166
+
167
+ for (const field of currentFields) {
168
+ if (!anchorFields.has(field)) added++;
169
+ }
170
+ for (const field of anchorFields) {
171
+ if (!currentFields.has(field)) removed++;
172
+ }
173
+
174
+ // "Retyped" detection: walk both schemas recursively and compare the JSON
175
+ // type of every fully-qualified field path that exists in both versions.
176
+ // This catches retypes nested inside object properties (where the path
177
+ // stays the same but the type changes), which a top-level-only diff
178
+ // would miss and silently undercount.
179
+ if (anchorSchema) {
180
+ const currentTypes = collectFieldTypes(currentSchema);
181
+ const anchorTypes = collectFieldTypes(anchorSchema);
182
+ for (const [path, curType] of currentTypes) {
183
+ if (anchorTypes.has(path)) {
184
+ const anType = anchorTypes.get(path);
185
+ if (curType !== anType) retyped++;
186
+ }
187
+ }
188
+ }
189
+ }
190
+
191
+ return { added, removed, retyped, total: totalCurrentFields };
192
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * decision-event-emit.js — Shared emit helper for COMP-OBS-TIMELINE.
3
+ *
4
+ * Central choke point for all DecisionEvent broadcasts. Builders produce
5
+ * contract-clean event shapes; emitDecisionEvent wraps them in the canonical
6
+ * envelope matching BRANCH's existing pattern (cc-session-watcher.js:151-166):
7
+ * { type: 'decisionEvent', event: { id, feature_code, timestamp, kind, title, metadata, roles } }
8
+ *
9
+ * Outcome mapping for iteration.metadata.outcome:
10
+ * server outcome → schema enum
11
+ * 'clean' → 'pass'
12
+ * 'max_reached' → 'fail'
13
+ * 'aborted' → 'fail'
14
+ * 'timeout' → 'fail'
15
+ * 'action_limit' → 'fail'
16
+ * 'retry' / null → 'retry'
17
+ */
18
+
19
+ import {
20
+ phaseTransitionDecisionEventId,
21
+ iterationDecisionEventId,
22
+ gateDecisionEventId,
23
+ driftThresholdDecisionEventId,
24
+ } from './decision-event-id.js';
25
+ import { mapResolveOutcomeToSchema } from './gate-log-store.js';
26
+
27
+ // Map server-side iteration outcome strings to the schema enum {pass|fail|retry}
28
+ function mapIterationOutcome(outcome) {
29
+ if (!outcome || outcome === 'retry') return 'retry';
30
+ if (outcome === 'clean' || outcome === 'pass') return 'pass';
31
+ // max_reached | aborted | timeout | action_limit → fail
32
+ return 'fail';
33
+ }
34
+
35
+ /**
36
+ * Emit a DecisionEvent over broadcastMessage using the canonical envelope.
37
+ * The envelope matches BRANCH's _buildForkEvent / broadcastMessage pattern.
38
+ */
39
+ export function emitDecisionEvent(broadcastMessage, event) {
40
+ broadcastMessage({ type: 'decisionEvent', event });
41
+ }
42
+
43
+ /**
44
+ * Build a kind=phase_transition DecisionEvent.
45
+ *
46
+ * @param {{ featureCode, from, to, outcome, agent_id, timestamp }} params
47
+ * - from: previous phase string, or null for the initial lifecycle start
48
+ * - to: new phase string
49
+ * - outcome: optional outcome string (for context)
50
+ * - agent_id: optional operator/agent identifier
51
+ */
52
+ export function buildPhaseTransitionEvent({ featureCode, from, to, outcome, agent_id, timestamp }) {
53
+ const now = timestamp || new Date().toISOString();
54
+ const fromStr = from == null ? 'null' : String(from);
55
+ const id = phaseTransitionDecisionEventId(featureCode, from, to, now);
56
+ return {
57
+ id,
58
+ feature_code: featureCode,
59
+ timestamp: now,
60
+ kind: 'phase_transition',
61
+ title: from == null
62
+ ? `Lifecycle started: ${to}`
63
+ : `Phase: ${from} → ${to}${outcome === 'skipped' ? ' (skipped)' : outcome === 'killed' ? ' (killed)' : ''}`,
64
+ metadata: {
65
+ from_phase: fromStr,
66
+ to_phase: String(to),
67
+ },
68
+ roles: [{ name: 'PRODUCER', agent_id: agent_id || null }],
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Build a kind=iteration DecisionEvent.
74
+ * Only called at loop start and loop complete — NOT per-attempt.
75
+ *
76
+ * @param {{ featureCode, loopId, loopType, stage, attempt, outcome, timestamp }} params
77
+ * - stage: 'start' | 'complete'
78
+ * - loopType: 'review' | 'coverage' | other
79
+ * - attempt: iteration count at the time of this event (0 for start)
80
+ * - outcome: server outcome string (see mapping above)
81
+ */
82
+ export function buildIterationEvent({ featureCode, loopId, loopType, stage, attempt, outcome, timestamp }) {
83
+ const now = timestamp || new Date().toISOString();
84
+ const id = iterationDecisionEventId(featureCode, loopId, stage);
85
+ const schemaOutcome = mapIterationOutcome(outcome);
86
+
87
+ // Role assignment per design Decision 5
88
+ let roles = [];
89
+ if (loopType === 'review') {
90
+ roles = [{ name: 'REVIEWER', agent_id: null }];
91
+ } else if (loopType === 'coverage') {
92
+ roles = [{ name: 'IMPLEMENTER', agent_id: null }];
93
+ }
94
+
95
+ // Title: descriptive for start vs complete
96
+ let title;
97
+ if (stage === 'start') {
98
+ title = `Iteration loop started — ${loopType}`;
99
+ } else {
100
+ const cnt = attempt != null ? ` (${attempt} attempt${attempt !== 1 ? 's' : ''})` : '';
101
+ title = `Iteration loop complete — ${loopType}${cnt}`;
102
+ }
103
+
104
+ const metadata = { iteration_id: loopId };
105
+ // attempt is schema-optional; schema requires minimum: 1 so only include when >= 1
106
+ if (attempt != null && attempt >= 1) metadata.attempt = attempt;
107
+ if (stage !== 'start') metadata.outcome = schemaOutcome;
108
+
109
+ return {
110
+ id,
111
+ feature_code: featureCode,
112
+ timestamp: now,
113
+ kind: 'iteration',
114
+ title,
115
+ metadata,
116
+ roles,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Build a kind=drift_threshold DecisionEvent.
122
+ *
123
+ * Called by emitDriftAxes on the rising edge (breached false → true).
124
+ * id and timestamp are taken from the persisted breach_event_id and
125
+ * breach_started_at — NOT recomputed — so live-emit and snapshot rehydration
126
+ * produce byte-for-byte identical events.
127
+ *
128
+ * @param {{ featureCode, axisId, ratio, threshold, breachStartedAt, breachEventId }} params
129
+ */
130
+ export function buildDriftThresholdEvent({ featureCode, axisId, ratio, threshold, breachStartedAt, breachEventId }) {
131
+ // If caller omits breachEventId, derive it deterministically (fallback safety).
132
+ const id = breachEventId || driftThresholdDecisionEventId(featureCode, axisId, breachStartedAt);
133
+ return {
134
+ id,
135
+ feature_code: featureCode,
136
+ timestamp: breachStartedAt,
137
+ kind: 'drift_threshold',
138
+ title: `Drift threshold crossed: ${axisId} (${Math.round(ratio * 100)}% ≥ ${Math.round(threshold * 100)}%)`,
139
+ metadata: {
140
+ axis_id: axisId,
141
+ ratio,
142
+ threshold,
143
+ },
144
+ roles: [],
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Build a kind=gate DecisionEvent.
150
+ *
151
+ * @param {{ featureCode, gateLogEntryId, gateId, decision, timestamp }} params
152
+ * - decision: route outcome string (approve|revise|kill) — translated to schema enum internally
153
+ */
154
+ export function buildGateEvent({ featureCode, gateLogEntryId, gateId, decision, timestamp }) {
155
+ const now = timestamp || new Date().toISOString();
156
+ const id = gateDecisionEventId(featureCode, gateLogEntryId);
157
+ const schemaDecision = mapResolveOutcomeToSchema(decision);
158
+
159
+ const outcomeLabels = { approve: 'approved', interrupt: 'interrupted', deny: 'denied' };
160
+ const label = outcomeLabels[schemaDecision] || schemaDecision;
161
+
162
+ return {
163
+ id,
164
+ feature_code: featureCode,
165
+ timestamp: now,
166
+ kind: 'gate',
167
+ title: `Gate ${label}: ${gateId}`,
168
+ metadata: {
169
+ gate_id: gateId,
170
+ decision: schemaDecision,
171
+ gate_log_entry_id: gateLogEntryId,
172
+ },
173
+ roles: [],
174
+ };
175
+ }
@@ -0,0 +1,64 @@
1
+ import { v5 as uuidv5 } from 'uuid';
2
+
3
+ const ROOT_NAMESPACE = '3a7c1b12-9c10-4d02-ae9e-5f0e8bf3b2e1';
4
+
5
+ export function branchDecisionEventId(featureCode, branchId) {
6
+ const featureNs = uuidv5(String(featureCode), ROOT_NAMESPACE);
7
+ return uuidv5(`branch:${branchId}`, featureNs);
8
+ }
9
+
10
+ export function shouldEmit(eventId, emittedSet) {
11
+ if (!emittedSet) return true;
12
+ if (emittedSet instanceof Set) return !emittedSet.has(eventId);
13
+ if (Array.isArray(emittedSet)) return !emittedSet.includes(eventId);
14
+ return true;
15
+ }
16
+
17
+ /**
18
+ * Deterministic id for a phase_transition DecisionEvent.
19
+ * Unique per (featureCode, fromPhase, toPhase, timestamp).
20
+ */
21
+ export function phaseTransitionDecisionEventId(featureCode, fromPhase, toPhase, timestamp) {
22
+ const featureNs = uuidv5(String(featureCode), ROOT_NAMESPACE);
23
+ const from = fromPhase == null ? 'null' : String(fromPhase);
24
+ return uuidv5(`phase_transition:${from}:${toPhase}:${timestamp}`, featureNs);
25
+ }
26
+
27
+ /**
28
+ * Deterministic id for an iteration DecisionEvent.
29
+ * stage ∈ 'start' | 'complete'
30
+ */
31
+ export function iterationDecisionEventId(featureCode, loopId, stage) {
32
+ const featureNs = uuidv5(String(featureCode), ROOT_NAMESPACE);
33
+ return uuidv5(`iteration:${loopId}:${stage}`, featureNs);
34
+ }
35
+
36
+ /**
37
+ * Deterministic id for a gate DecisionEvent (kind='gate').
38
+ * Unique per (featureCode, gateLogEntryId).
39
+ * Once entry.id is fixed, the event id is fixed — enabling reconciliation.
40
+ *
41
+ * @param {string} featureCode
42
+ * @param {string} gateLogEntryId — UUID v4 of the GateLogEntry
43
+ * @returns {string} UUID v5
44
+ */
45
+ export function gateDecisionEventId(featureCode, gateLogEntryId) {
46
+ const featureNs = uuidv5(String(featureCode), ROOT_NAMESPACE);
47
+ return uuidv5(`gate:${gateLogEntryId}`, featureNs);
48
+ }
49
+
50
+ /**
51
+ * Deterministic id for a drift_threshold DecisionEvent.
52
+ * Unique per (featureCode, axisId, breachStartedAt).
53
+ * Persisted on the DriftAxis as breach_event_id so rehydration produces the
54
+ * same id as the live emit without recomputing from current computed_at.
55
+ *
56
+ * @param {string} featureCode
57
+ * @param {string} axisId — 'path_drift' | 'contract_drift' | 'review_debt_drift'
58
+ * @param {string} breachStartedAtIso — ISO timestamp of the rising-edge breach
59
+ * @returns {string} UUID v5
60
+ */
61
+ export function driftThresholdDecisionEventId(featureCode, axisId, breachStartedAtIso) {
62
+ const featureNs = uuidv5(String(featureCode), ROOT_NAMESPACE);
63
+ return uuidv5(`drift_threshold:${axisId}:${breachStartedAtIso}`, featureNs);
64
+ }