@duo-director/cli 0.2.1 → 0.3.0-rc.3

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 (81) hide show
  1. package/dist/THIRD_PARTY_NOTICES.md +3 -3
  2. package/dist/{cli-WTCBCBVN.js → cli-DJGVC2WO.js} +881 -407
  3. package/dist/duoctl.js +1 -1
  4. package/dist/runtime-tree.json +12 -12
  5. package/dist/ui/app.js +6 -6
  6. package/node_modules/@modelcontextprotocol/client/dist/{index-CEk3qkZM.d.cts → index-D_CiY-T3.d.cts} +3 -1
  7. package/node_modules/@modelcontextprotocol/client/dist/index-D_CiY-T3.d.cts.map +1 -0
  8. package/node_modules/@modelcontextprotocol/client/dist/{index-BCtJi0p5.d.mts → index-D_QDtbiv.d.mts} +3 -1
  9. package/node_modules/@modelcontextprotocol/client/dist/index-D_QDtbiv.d.mts.map +1 -0
  10. package/node_modules/@modelcontextprotocol/client/dist/index.cjs +100 -55
  11. package/node_modules/@modelcontextprotocol/client/dist/index.cjs.map +1 -1
  12. package/node_modules/@modelcontextprotocol/client/dist/index.d.cts +157 -30
  13. package/node_modules/@modelcontextprotocol/client/dist/index.d.cts.map +1 -1
  14. package/node_modules/@modelcontextprotocol/client/dist/index.d.mts +157 -30
  15. package/node_modules/@modelcontextprotocol/client/dist/index.d.mts.map +1 -1
  16. package/node_modules/@modelcontextprotocol/client/dist/index.mjs +100 -55
  17. package/node_modules/@modelcontextprotocol/client/dist/index.mjs.map +1 -1
  18. package/node_modules/@modelcontextprotocol/client/dist/shimsWorkerd.cjs +1 -1
  19. package/node_modules/@modelcontextprotocol/client/dist/shimsWorkerd.mjs +1 -1
  20. package/node_modules/@modelcontextprotocol/client/dist/{src-D0qEN74R.cjs → src-oZBjf3U1.cjs} +21 -4
  21. package/node_modules/@modelcontextprotocol/client/dist/src-oZBjf3U1.cjs.map +1 -0
  22. package/node_modules/@modelcontextprotocol/client/dist/{src-DDAAhLnO.mjs → src-xKF0PJN-.mjs} +16 -5
  23. package/node_modules/@modelcontextprotocol/client/dist/src-xKF0PJN-.mjs.map +1 -0
  24. package/node_modules/@modelcontextprotocol/client/dist/stdio.cjs +1 -1
  25. package/node_modules/@modelcontextprotocol/client/dist/stdio.d.cts +1 -1
  26. package/node_modules/@modelcontextprotocol/client/dist/stdio.d.mts +1 -1
  27. package/node_modules/@modelcontextprotocol/client/dist/stdio.mjs +1 -1
  28. package/node_modules/@modelcontextprotocol/client/package.json +3 -3
  29. package/node_modules/@modelcontextprotocol/core/dist/{auth--n_Yzytt.d.cts → auth-Anm-lwWi.d.mts} +9 -8
  30. package/node_modules/@modelcontextprotocol/core/dist/{auth--n_Yzytt.d.cts.map → auth-Anm-lwWi.d.mts.map} +1 -1
  31. package/node_modules/@modelcontextprotocol/core/dist/{auth-CGP0BDVq.mjs → auth-BNDyLTqp.mjs} +5 -3
  32. package/node_modules/@modelcontextprotocol/core/dist/{auth-CGP0BDVq.mjs.map → auth-BNDyLTqp.mjs.map} +1 -1
  33. package/node_modules/@modelcontextprotocol/core/dist/{auth-4-zilcqg.cjs → auth-D1CeV_Jr.cjs} +5 -3
  34. package/node_modules/@modelcontextprotocol/core/dist/{auth-4-zilcqg.cjs.map → auth-D1CeV_Jr.cjs.map} +1 -1
  35. package/node_modules/@modelcontextprotocol/core/dist/{auth-YUQV3RRv.d.mts → auth-V9XJSCfa.d.cts} +9 -8
  36. package/node_modules/@modelcontextprotocol/core/dist/{auth-YUQV3RRv.d.mts.map → auth-V9XJSCfa.d.cts.map} +1 -1
  37. package/node_modules/@modelcontextprotocol/core/dist/index.cjs +1 -1
  38. package/node_modules/@modelcontextprotocol/core/dist/index.d.cts +1 -1
  39. package/node_modules/@modelcontextprotocol/core/dist/index.d.mts +1 -1
  40. package/node_modules/@modelcontextprotocol/core/dist/index.mjs +1 -1
  41. package/node_modules/@modelcontextprotocol/core/dist/internal.cjs +1 -1
  42. package/node_modules/@modelcontextprotocol/core/dist/internal.d.cts +1 -1
  43. package/node_modules/@modelcontextprotocol/core/dist/internal.d.mts +1 -1
  44. package/node_modules/@modelcontextprotocol/core/dist/internal.mjs +1 -1
  45. package/node_modules/@modelcontextprotocol/core/package.json +1 -1
  46. package/node_modules/@modelcontextprotocol/server/dist/{createMcpHandler-Bt6U_Fqb.d.mts → createMcpHandler-CfX6zgs1.d.mts} +3 -1
  47. package/node_modules/@modelcontextprotocol/server/dist/createMcpHandler-CfX6zgs1.d.mts.map +1 -0
  48. package/node_modules/@modelcontextprotocol/server/dist/{createMcpHandler-BLVe3qmU.d.cts → createMcpHandler-LSKhyqpU.d.cts} +3 -1
  49. package/node_modules/@modelcontextprotocol/server/dist/{createMcpHandler-BLVe3qmU.d.cts.map → createMcpHandler-LSKhyqpU.d.cts.map} +1 -1
  50. package/node_modules/@modelcontextprotocol/server/dist/index.cjs +5 -3
  51. package/node_modules/@modelcontextprotocol/server/dist/index.cjs.map +1 -1
  52. package/node_modules/@modelcontextprotocol/server/dist/index.d.cts +1 -1
  53. package/node_modules/@modelcontextprotocol/server/dist/index.d.mts +1 -1
  54. package/node_modules/@modelcontextprotocol/server/dist/index.d.mts.map +1 -1
  55. package/node_modules/@modelcontextprotocol/server/dist/index.mjs +5 -3
  56. package/node_modules/@modelcontextprotocol/server/dist/index.mjs.map +1 -1
  57. package/node_modules/@modelcontextprotocol/server/dist/{mcp-Dw2OlZ1f.mjs → mcp-DYuW2ZSs.mjs} +6 -2
  58. package/node_modules/@modelcontextprotocol/server/dist/mcp-DYuW2ZSs.mjs.map +1 -0
  59. package/node_modules/@modelcontextprotocol/server/dist/{mcp-DUcCQSTW.cjs → mcp-TfXCpFsH.cjs} +6 -2
  60. package/node_modules/@modelcontextprotocol/server/dist/mcp-TfXCpFsH.cjs.map +1 -0
  61. package/node_modules/@modelcontextprotocol/server/dist/shimsWorkerd.cjs +1 -1
  62. package/node_modules/@modelcontextprotocol/server/dist/shimsWorkerd.mjs +1 -1
  63. package/node_modules/@modelcontextprotocol/server/dist/{src-D-y6h4N7.mjs → src-BHSMhZ_W.mjs} +8 -4
  64. package/node_modules/@modelcontextprotocol/server/dist/src-BHSMhZ_W.mjs.map +1 -0
  65. package/node_modules/@modelcontextprotocol/server/dist/{src-CgFsfg3E.cjs → src-BtQ5YSyq.cjs} +8 -4
  66. package/node_modules/@modelcontextprotocol/server/dist/src-BtQ5YSyq.cjs.map +1 -0
  67. package/node_modules/@modelcontextprotocol/server/dist/stdio.cjs +2 -2
  68. package/node_modules/@modelcontextprotocol/server/dist/stdio.d.cts +1 -1
  69. package/node_modules/@modelcontextprotocol/server/dist/stdio.d.mts +1 -1
  70. package/node_modules/@modelcontextprotocol/server/dist/stdio.mjs +2 -2
  71. package/node_modules/@modelcontextprotocol/server/package.json +3 -3
  72. package/package.json +3 -3
  73. package/node_modules/@modelcontextprotocol/client/dist/index-BCtJi0p5.d.mts.map +0 -1
  74. package/node_modules/@modelcontextprotocol/client/dist/index-CEk3qkZM.d.cts.map +0 -1
  75. package/node_modules/@modelcontextprotocol/client/dist/src-D0qEN74R.cjs.map +0 -1
  76. package/node_modules/@modelcontextprotocol/client/dist/src-DDAAhLnO.mjs.map +0 -1
  77. package/node_modules/@modelcontextprotocol/server/dist/createMcpHandler-Bt6U_Fqb.d.mts.map +0 -1
  78. package/node_modules/@modelcontextprotocol/server/dist/mcp-DUcCQSTW.cjs.map +0 -1
  79. package/node_modules/@modelcontextprotocol/server/dist/mcp-Dw2OlZ1f.mjs.map +0 -1
  80. package/node_modules/@modelcontextprotocol/server/dist/src-CgFsfg3E.cjs.map +0 -1
  81. package/node_modules/@modelcontextprotocol/server/dist/src-D-y6h4N7.mjs.map +0 -1
@@ -157,8 +157,12 @@ var DIAGNOSTIC_SEVERITY = {
157
157
  DECISION_SUPERSEDE_TARGET_INVALID: "error",
158
158
  /** DecisionService: another process holds the repository decision lock. */
159
159
  DECISION_LOCK_BUSY: "error",
160
+ /** DecisionService (T34.2): the candidate changed after the preview a person reviewed; nothing was confirmed. */
161
+ DECISION_CONFIRM_PREVIEW_CHANGED: "error",
160
162
  /** A confirmed Decision's lock.digest does not match its content fields (a detection aid, not a signature). */
161
163
  DECISION_LOCK_MISMATCH: "warning",
164
+ /** T40 (N1): an authoritative operation refused partial Project Truth; the loader errors follow it. */
165
+ PROJECT_TRUTH_INVALID: "error",
162
166
  /** A SourceLocation that does not address its canonical source text (a producer bug, T09.1). */
163
167
  SOURCE_LOCATION_INVALID: "error",
164
168
  /** Context Compiler: the request cannot be compiled (empty task, budget out of range or below the packet frame, unknown profile). */
@@ -298,6 +302,8 @@ var DIAGNOSTIC_PERSISTENCE = {
298
302
  DECISION_TARGET_UNSUPPORTED: T,
299
303
  DECISION_SUPERSEDE_TARGET_INVALID: T,
300
304
  DECISION_LOCK_BUSY: T,
305
+ DECISION_CONFIRM_PREVIEW_CHANGED: T,
306
+ PROJECT_TRUTH_INVALID: T,
301
307
  DECISION_LOCK_MISMATCH: P,
302
308
  SOURCE_LOCATION_INVALID: P,
303
309
  CONTEXT_REQUEST_INVALID: T,
@@ -1311,7 +1317,8 @@ var decisionShape = {
1311
1317
  rationale: text().optional(),
1312
1318
  owner: z.literal("human").optional(),
1313
1319
  governs: z.strictObject({ requirements: ids().optional(), paths: texts().optional(), symbols: texts().optional() }).optional(),
1314
- forbids: z.strictObject({ dependencies: texts().optional(), symbols: texts().optional(), paths: texts().optional() }).optional(),
1320
+ /** imported_paths (H-72): repository path patterns; a changed import resolving to a matching file conflicts. */
1321
+ forbids: z.strictObject({ dependencies: texts().optional(), symbols: texts().optional(), paths: texts().optional(), imported_paths: texts().optional() }).optional(),
1315
1322
  supersedes: id().nullable().optional(),
1316
1323
  evidence: z.array(EvidencePointerSchema).optional(),
1317
1324
  source: sources,
@@ -1592,7 +1599,9 @@ function mapDecisionContent(d, ctx, governsRelation) {
1592
1599
  forbids: {
1593
1600
  dependencies: d.forbids?.dependencies ?? [],
1594
1601
  symbols: d.forbids?.symbols ?? [],
1595
- paths: ctx.patterns(["forbids", "paths"], d.forbids?.paths)
1602
+ paths: ctx.patterns(["forbids", "paths"], d.forbids?.paths),
1603
+ // H-72: only when present and non-empty; absent stays absent (lock digest compatibility).
1604
+ ...(d.forbids?.imported_paths?.length ?? 0) > 0 ? { importedPaths: ctx.patterns(["forbids", "imported_paths"], d.forbids?.imported_paths) } : {}
1596
1605
  },
1597
1606
  supersedes: d.supersedes ?? null,
1598
1607
  evidence: ctx.evidence(["evidence"], d.evidence),
@@ -2183,6 +2192,23 @@ function loadProjectTruth(root, options = {}) {
2183
2192
  return { value: { truth, trace: trace2.model }, diagnostics: diagnostics.sort(compareDiagnostics) };
2184
2193
  }
2185
2194
 
2195
+ // packages/core/dist/loader/authority.js
2196
+ var NON_BLOCKING_TRUTH_ERRORS = /* @__PURE__ */ new Set(["BROKEN_REFERENCE", "REFERENCE_TYPE_MISMATCH"]);
2197
+ function truthAuthorityErrors(diagnostics) {
2198
+ return diagnostics.filter((d) => d.severity === "error" && !NON_BLOCKING_TRUTH_ERRORS.has(d.code));
2199
+ }
2200
+ function projectTruthInvalid(errors) {
2201
+ const files = [...new Set(errors.map((e) => e.source?.path).filter((p) => p !== void 0))];
2202
+ const where = files.length === 0 ? "" : `: ${files.slice(0, 3).join(", ")}${files.length > 3 ? ` and ${files.length - 3} more` : ""}`;
2203
+ return createDiagnostic("PROJECT_TRUTH_INVALID", `Project Truth could not be read completely (${errors.length} error${errors.length === 1 ? "" : "s"}${where}). DUO does not review, compile context or change Decisions on partial Truth. Fix the file; DUO does not change it.`);
2204
+ }
2205
+ function requireCompleteTruth(loaded) {
2206
+ if (loaded.value === void 0)
2207
+ return loaded;
2208
+ const errors = truthAuthorityErrors(loaded.diagnostics);
2209
+ return errors.length === 0 ? loaded : { diagnostics: [projectTruthInvalid(errors), ...errors] };
2210
+ }
2211
+
2186
2212
  // packages/core/dist/decisions/service.js
2187
2213
  import os from "node:os";
2188
2214
 
@@ -2199,6 +2225,9 @@ function canonical(value) {
2199
2225
  }
2200
2226
  return value;
2201
2227
  }
2228
+ function candidateDigest(value) {
2229
+ return sha256(stableJson(value));
2230
+ }
2202
2231
  function stableJson(value) {
2203
2232
  return JSON.stringify(canonical(value));
2204
2233
  }
@@ -2362,6 +2391,7 @@ var DECISION_PERMISSIONS = {
2362
2391
  confirm: ["human"],
2363
2392
  reject: ["human"]
2364
2393
  };
2394
+ var CANDIDATE_FIELDS = ["title", "kind", "question", "answer", "rationale", "governs", "forbids", "enforcement", "supersedes", "evidence", "source", "extensions"];
2365
2395
  var SYSTEM = { kind: "system", name: "duo" };
2366
2396
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2367
2397
  var idTokens = (message2) => message2.split(/[^A-Za-z0-9-]+/u);
@@ -2419,8 +2449,9 @@ function createDecisionService(options) {
2419
2449
  await fs28.remove(file);
2420
2450
  }
2421
2451
  }
2422
- function load() {
2423
- const loaded = loadProjectTruth(root);
2452
+ function load(complete = true) {
2453
+ const raw = loadProjectTruth(root);
2454
+ const loaded = complete ? requireCompleteTruth(raw) : raw;
2424
2455
  return loaded.value === void 0 ? failure(loaded.diagnostics) : success(loaded.value.truth, loaded.diagnostics);
2425
2456
  }
2426
2457
  const isServiceFile = (d) => d.location.path === decisionPath(d.id);
@@ -2475,13 +2506,13 @@ function createDecisionService(options) {
2475
2506
  }
2476
2507
  return { repaired, diagnostics };
2477
2508
  }
2478
- async function prepare() {
2479
- let loaded = load();
2509
+ async function prepare(complete = true) {
2510
+ let loaded = load(complete);
2480
2511
  if (loaded.value === void 0)
2481
2512
  return failure(loaded.diagnostics);
2482
2513
  const fixed = await repairWith(loaded.value);
2483
2514
  if (fixed.repaired.length > 0) {
2484
- loaded = load();
2515
+ loaded = load(complete);
2485
2516
  if (loaded.value === void 0)
2486
2517
  return failure(loaded.diagnostics);
2487
2518
  }
@@ -2541,7 +2572,7 @@ function createDecisionService(options) {
2541
2572
  if (denied.length > 0)
2542
2573
  return failure(denied);
2543
2574
  return withLock(async () => {
2544
- const prepared = await prepare();
2575
+ const prepared = await prepare(false);
2545
2576
  if (prepared.value === void 0)
2546
2577
  return failure(prepared.diagnostics);
2547
2578
  const { truth, repaired } = prepared.value;
@@ -2588,7 +2619,7 @@ function createDecisionService(options) {
2588
2619
  return success({ proposalId: created.value.id, path: created.value.path, indexRequired: false, repaired }, prepared.diagnostics);
2589
2620
  });
2590
2621
  },
2591
- async confirm(actor, id3) {
2622
+ async confirm(actor, id3, confirmOptions = {}) {
2592
2623
  const denied = forbidden(actor, "confirm");
2593
2624
  if (denied.length > 0)
2594
2625
  return failure(denied);
@@ -2600,7 +2631,13 @@ function createDecisionService(options) {
2600
2631
  return failure(prepared.diagnostics);
2601
2632
  const { truth, repaired } = prepared.value;
2602
2633
  const warnings = [...prepared.diagnostics];
2603
- return DECISION_ID.test(id3) ? confirmDecision(actor, id3, truth, repaired, warnings) : confirmProposal(actor, id3, truth, repaired, warnings);
2634
+ const c = await candidateOf(truth, id3);
2635
+ if (c.value === void 0)
2636
+ return failure(c.diagnostics);
2637
+ if (confirmOptions.expectedDigest !== void 0 && confirmOptions.expectedDigest !== c.value.digest) {
2638
+ return failure([createDiagnostic("DECISION_CONFIRM_PREVIEW_CHANGED", "The Decision changed after you reviewed it. Review the current contents and confirm again.", { path: c.value.sourcePath })]);
2639
+ }
2640
+ return c.value.sourceKind === "decision" ? confirmDecision(actor, c.value, truth, repaired, warnings) : confirmProposal(actor, c.value, truth, repaired, warnings);
2604
2641
  });
2605
2642
  },
2606
2643
  async reject(actor, proposalId, reason) {
@@ -2635,39 +2672,70 @@ function createDecisionService(options) {
2635
2672
  return prepared.value === void 0 ? failure(prepared.diagnostics) : success({ repaired: prepared.value.repaired }, prepared.diagnostics);
2636
2673
  });
2637
2674
  },
2638
- async previewConfirm(proposalId) {
2639
- if (!PROPOSAL_ID_PATTERN.test(proposalId))
2640
- return failure([createDiagnostic("INVALID_ID", `"${proposalId}" is not a proposal ID`)]);
2675
+ async previewConfirm(id3) {
2676
+ if (!PROPOSAL_ID_PATTERN.test(id3) && !DECISION_ID.test(id3))
2677
+ return failure([createDiagnostic("INVALID_ID", `"${id3}" is not a proposal or Decision ID`)]);
2641
2678
  const loaded = load();
2642
2679
  if (loaded.value === void 0)
2643
2680
  return failure(loaded.diagnostics);
2644
2681
  const truth = loaded.value;
2645
- const entry = listDecisionProposals(truth).find((p) => p.id === proposalId);
2646
- if (entry === void 0)
2647
- return failure([createDiagnostic("PROPOSAL_NOT_FOUND", `No proposal ${proposalId}`)]);
2648
- if (entry.status !== "pending")
2649
- return failure([createDiagnostic("PROPOSAL_NOT_PENDING", `${proposalId} is ${entry.status}`, entry.proposal.location)]);
2650
- const stale = staleness(truth, entry.proposal);
2651
- const target = entry.proposal.supersedes === null ? void 0 : truth.decisions.find((d) => d.id === entry.proposal.supersedes);
2682
+ const c = await candidateOf(truth, id3);
2683
+ if (c.value === void 0)
2684
+ return failure(c.diagnostics);
2685
+ const v = c.value;
2686
+ const expected = v.sourceKind === "decision" ? id3 : await allocate("D", truth);
2652
2687
  return success({
2653
- proposalId,
2654
- nextDecisionId: await allocate("D", truth),
2655
- ...stale === void 0 ? {} : { stale },
2656
- ...target === void 0 ? {} : { supersedes: { id: target.id, title: target.title, state: target.state, path: target.location.path } }
2688
+ sourceId: id3,
2689
+ sourceKind: v.sourceKind,
2690
+ sourcePath: v.sourcePath,
2691
+ action: v.action,
2692
+ ...v.sourceKind === "proposal" ? { proposalId: id3 } : {},
2693
+ expectedDecisionId: expected,
2694
+ nextDecisionId: expected,
2695
+ candidate: v.candidate,
2696
+ ...v.proposedBy === void 0 ? {} : { proposedBy: v.proposedBy },
2697
+ ...v.proposedByKind === void 0 ? {} : { proposedByKind: v.proposedByKind },
2698
+ ...v.stale === void 0 ? {} : { stale: v.stale },
2699
+ ...v.target === void 0 ? {} : { supersedes: { id: v.target.id, title: v.target.title, state: v.target.state, path: v.target.location.path } },
2700
+ digest: v.digest
2657
2701
  });
2658
2702
  },
2659
- verifyLock(decisionId) {
2703
+ verifyLock(decisionId2) {
2660
2704
  const loaded = load();
2661
2705
  if (loaded.value === void 0)
2662
2706
  return failure(loaded.diagnostics);
2663
- const d = loaded.value.decisions.find((x) => x.id === decisionId);
2707
+ const d = loaded.value.decisions.find((x) => x.id === decisionId2);
2664
2708
  if (d === void 0)
2665
- return failure([createDiagnostic("PROPOSAL_NOT_FOUND", `No Decision ${decisionId}`)]);
2709
+ return failure([createDiagnostic("PROPOSAL_NOT_FOUND", `No Decision ${decisionId2}`)]);
2666
2710
  const r = verifyDecisionLock(d);
2667
2711
  return success(r.value, r.diagnostics);
2668
2712
  }
2669
2713
  };
2670
- async function confirmProposal(actor, id3, truth, repaired, warnings) {
2714
+ async function candidateOf(truth, id3) {
2715
+ if (DECISION_ID.test(id3)) {
2716
+ const d = truth.decisions.find((x) => x.id === id3);
2717
+ if (d === void 0)
2718
+ return failure([createDiagnostic("PROPOSAL_NOT_FOUND", `No Decision ${id3}`)]);
2719
+ if (!isServiceFile(d))
2720
+ return failure([createDiagnostic("DECISION_TARGET_UNSUPPORTED", `${id3} is not a decisions/D-###.yaml file; DUO does not rewrite it`, d.location)]);
2721
+ if (d.state === "superseded" || d.state === CONFIRMED && d.lock !== void 0) {
2722
+ return failure([createDiagnostic("DECISION_LOCKED", `${id3} is ${d.state} and locked; supersede it with a new proposal`, d.location)]);
2723
+ }
2724
+ if (d.state === "rejected")
2725
+ return failure([createDiagnostic("PROPOSAL_NOT_PENDING", `${id3} is rejected`, d.location)]);
2726
+ const data2 = await rawData(d.location.path);
2727
+ if (data2 === void 0)
2728
+ return failure([createDiagnostic("PROPOSAL_NOT_FOUND", `${d.location.path} is missing`)]);
2729
+ return success(finishCandidate(truth, {
2730
+ sourceId: id3,
2731
+ sourceKind: "decision",
2732
+ sourcePath: d.location.path,
2733
+ action: d.state === CONFIRMED ? "add-lock" : "confirm-in-place",
2734
+ data: data2,
2735
+ supersedesId: d.supersedes,
2736
+ decision: d
2737
+ }));
2738
+ }
2671
2739
  const done = truth.decisions.find((d) => d.proposalId === id3);
2672
2740
  if (done !== void 0)
2673
2741
  return failure([createDiagnostic("PROPOSAL_NOT_PENDING", `${id3} was already confirmed as ${done.id}`)]);
@@ -2676,16 +2744,54 @@ function createDecisionService(options) {
2676
2744
  return failure([createDiagnostic("PROPOSAL_NOT_FOUND", `No proposal ${id3}`)]);
2677
2745
  if (entry.status !== "pending")
2678
2746
  return failure([createDiagnostic("PROPOSAL_NOT_PENDING", `${id3} is ${entry.status}`, entry.proposal.location)]);
2679
- const proposal = entry.proposal;
2680
- const stale = staleness(truth, proposal);
2747
+ const data = await rawData(entry.proposal.location.path);
2748
+ if (data === void 0)
2749
+ return failure([createDiagnostic("PROPOSAL_NOT_FOUND", `${entry.proposal.location.path} is missing`)]);
2750
+ const stale = staleness(truth, entry.proposal);
2751
+ return success(finishCandidate(truth, {
2752
+ sourceId: id3,
2753
+ sourceKind: "proposal",
2754
+ sourcePath: entry.proposal.location.path,
2755
+ action: "create",
2756
+ data,
2757
+ supersedesId: entry.proposal.supersedes,
2758
+ proposal: entry.proposal,
2759
+ ...stale === void 0 ? {} : { stale },
2760
+ ...typeof data.proposed_by === "string" ? { proposedBy: data.proposed_by } : {},
2761
+ ...typeof data.proposed_by_kind === "string" ? { proposedByKind: data.proposed_by_kind } : {}
2762
+ }));
2763
+ }
2764
+ async function rawData(repoPath3) {
2765
+ const text4 = await fs28.readText(`${root}/${repoPath3}`);
2766
+ if (text4 === void 0)
2767
+ return void 0;
2768
+ return parseYaml({ path: repoPath3, text: text4 }).value?.data ?? {};
2769
+ }
2770
+ function finishCandidate(truth, base) {
2771
+ const candidate = defined(Object.fromEntries(CANDIDATE_FIELDS.map((k) => [k, base.data[k]])));
2772
+ const target = base.supersedesId === null ? void 0 : truth.decisions.find((t2) => t2.id === base.supersedesId);
2773
+ const digest = candidateDigest({
2774
+ sourceId: base.sourceId,
2775
+ sourceKind: base.sourceKind,
2776
+ sourcePath: base.sourcePath,
2777
+ action: base.action,
2778
+ candidate,
2779
+ stale: base.stale ?? null,
2780
+ proposedBy: base.proposedBy ?? null,
2781
+ proposedByKind: base.proposedByKind ?? null,
2782
+ supersedes: base.supersedesId === null ? null : target === void 0 ? { id: base.supersedesId, missing: true } : { id: target.id, title: target.title, state: target.state, supersededBy: target.supersededBy, path: target.location.path, content: decisionLockDigest(target) }
2783
+ });
2784
+ return { ...base, candidate, ...target === void 0 ? {} : { target }, digest };
2785
+ }
2786
+ async function confirmProposal(actor, c, truth, repaired, warnings) {
2787
+ const id3 = c.sourceId;
2788
+ const proposal = c.proposal;
2789
+ const stale = c.stale;
2681
2790
  if (stale !== void 0) {
2682
2791
  warnings.push(createDiagnostic("PROPOSAL_STALE", `${id3} was made on an older Project Truth${stale.changedRefs.length > 0 ? ` (changed: ${stale.changedRefs.join(", ")})` : ""}; confirmed anyway`, proposal.location));
2683
2792
  }
2684
- const text4 = await fs28.readText(`${root}/${proposal.location.path}`);
2685
- if (text4 === void 0)
2686
- return failure([createDiagnostic("PROPOSAL_NOT_FOUND", `${proposal.location.path} is missing`)]);
2687
- const data = parseYaml({ path: proposal.location.path, text: text4 }).value?.data ?? {};
2688
- const target = proposal.supersedes === null ? void 0 : truth.decisions.find((d) => d.id === proposal.supersedes);
2793
+ const data = c.data;
2794
+ const target = c.target;
2689
2795
  if (proposal.supersedes !== null) {
2690
2796
  if (target === void 0)
2691
2797
  return failure([createDiagnostic("BROKEN_REFERENCE", `${id3} supersedes ${proposal.supersedes}, which does not exist`, proposal.location)]);
@@ -2695,8 +2801,8 @@ function createDecisionService(options) {
2695
2801
  return failure([createDiagnostic("DECISION_SUPERSEDE_TARGET_INVALID", `${target.id} is ${target.state}; only a confirmed Decision can be superseded`, target.location)]);
2696
2802
  }
2697
2803
  const confirmedAt = now();
2698
- const content = (decisionId, digest) => yaml(defined({
2699
- id: decisionId,
2804
+ const content = (decisionId2, digest) => yaml(defined({
2805
+ id: decisionId2,
2700
2806
  title: data.title,
2701
2807
  kind: data.kind,
2702
2808
  state: CONFIRMED,
@@ -2719,9 +2825,9 @@ function createDecisionService(options) {
2719
2825
  confirmed_by: actor.name,
2720
2826
  lock: digest === void 0 ? void 0 : { digest }
2721
2827
  }));
2722
- const render = (decisionId) => {
2723
- const draft = parseDecisionFile(decisionPath(decisionId), content(decisionId));
2724
- return content(decisionId, draft.value === void 0 ? void 0 : decisionLockDigest(draft.value));
2828
+ const render = (decisionId2) => {
2829
+ const draft = parseDecisionFile(decisionPath(decisionId2), content(decisionId2));
2830
+ return content(decisionId2, draft.value === void 0 ? void 0 : decisionLockDigest(draft.value));
2725
2831
  };
2726
2832
  const probeId = await allocate("D", truth);
2727
2833
  const probe = parseDecisionFile(decisionPath(probeId), render(probeId));
@@ -2754,17 +2860,9 @@ function createDecisionService(options) {
2754
2860
  repaired
2755
2861
  }, [...warnings, ...after.map((d) => ({ ...d, severity: "warning" }))]);
2756
2862
  }
2757
- async function confirmDecision(actor, id3, truth, repaired, warnings) {
2758
- const d = truth.decisions.find((x) => x.id === id3);
2759
- if (d === void 0)
2760
- return failure([createDiagnostic("PROPOSAL_NOT_FOUND", `No Decision ${id3}`)]);
2761
- if (!isServiceFile(d))
2762
- return failure([createDiagnostic("DECISION_TARGET_UNSUPPORTED", `${id3} is not a decisions/D-###.yaml file; DUO does not rewrite it`, d.location)]);
2763
- if (d.state === "superseded" || d.state === CONFIRMED && d.lock !== void 0) {
2764
- return failure([createDiagnostic("DECISION_LOCKED", `${id3} is ${d.state} and locked; supersede it with a new proposal`, d.location)]);
2765
- }
2766
- if (d.state === "rejected")
2767
- return failure([createDiagnostic("PROPOSAL_NOT_PENDING", `${id3} is rejected`, d.location)]);
2863
+ async function confirmDecision(actor, c, truth, repaired, warnings) {
2864
+ const id3 = c.sourceId;
2865
+ const d = c.decision;
2768
2866
  const digest = decisionLockDigest(d);
2769
2867
  const values = d.state === CONFIRMED ? { lock: { digest }, confirmed_at: d.confirmedAt ?? now(), confirmed_by: d.confirmedBy ?? actor.name } : { state: CONFIRMED, owner: "human", confirmed_at: now(), confirmed_by: actor.name, lock: { digest } };
2770
2868
  const target = d.supersedes === null ? void 0 : truth.decisions.find((t2) => t2.id === d.supersedes);
@@ -9533,6 +9631,45 @@ function sameFingerprint(a, b) {
9533
9631
  return a !== void 0 && b !== void 0 && a.state === b.state && a.fingerprintMode === b.fingerprintMode && a.contentHash === b.contentHash && a.size === b.size && a.gitBlobOid === b.gitBlobOid;
9534
9632
  }
9535
9633
 
9634
+ // packages/graph/dist/incremental/references.js
9635
+ async function readIndexedModuleReferences(root, paths) {
9636
+ const read2 = readIndexState(root);
9637
+ if (read2.state === void 0)
9638
+ return paths.map((path35) => ({ status: "unavailable", path: path35, reason: read2.problem ?? "index state" }));
9639
+ const files = new Map(read2.state.files.map((f) => [f.path, f]));
9640
+ const keys = paths.map((path35) => {
9641
+ const f = files.get(path35);
9642
+ return f?.analysis === void 0 || f.analysis.status !== "ok" || f.resolution === void 0 ? void 0 : { path: path35, contentHash: f.contentHash, analyzer: f.analysis.analyzer, analyzerIdentity: f.analysis.identity };
9643
+ });
9644
+ const present = keys.filter((k) => k !== void 0);
9645
+ const cached = await readCachedAnalyses(root, present);
9646
+ const byPath = new Map(present.map((k, i) => [k.path, cached[i]]));
9647
+ return paths.map((path35, i) => {
9648
+ const f = files.get(path35);
9649
+ if (f === void 0)
9650
+ return { status: "not-indexed", path: path35 };
9651
+ if (keys[i] === void 0)
9652
+ return { status: "not-analyzed", path: path35 };
9653
+ const entry = byPath.get(path35);
9654
+ const modules = f.resolution?.modules ?? [];
9655
+ if (entry === void 0)
9656
+ return { status: "unavailable", path: path35, reason: "analysis cache entry missing" };
9657
+ const refs = entry.analysis.moduleReferences;
9658
+ if (refs.length !== modules.length)
9659
+ return { status: "unavailable", path: path35, reason: "analysis and resolution do not match" };
9660
+ return {
9661
+ status: "ok",
9662
+ path: path35,
9663
+ language: entry.analysis.language,
9664
+ references: refs.map((r, j) => {
9665
+ const resolution = modules[j];
9666
+ const target = resolution.status === "resolved" && resolution.path !== path35 && files.has(resolution.path) ? resolution.path : void 0;
9667
+ return { kind: r.kind, specifier: r.specifier, typeOnly: r.typeOnly, location: r.location, resolution, ...target === void 0 ? {} : { target } };
9668
+ })
9669
+ };
9670
+ });
9671
+ }
9672
+
9536
9673
  // packages/graph/dist/query/impact.js
9537
9674
  var RANK = { direct: 0, structural: 1, historical: 2 };
9538
9675
  var STEPS = {
@@ -10131,6 +10268,27 @@ function testRunEvidence(store, run2, test) {
10131
10268
  metadata: { status, ...run2.command === void 0 ? {} : { command: run2.command } }
10132
10269
  });
10133
10270
  }
10271
+ function moduleReferenceEvidence(store, reader, r, commit) {
10272
+ const slice = reader.slice(r.location);
10273
+ if (slice.value === void 0)
10274
+ return void 0;
10275
+ const contentHash = sha256Text(slice.value);
10276
+ const range = lines(r.location);
10277
+ return store.add({
10278
+ id: evidenceId("repository", "file", `${r.path}
10279
+ module-reference
10280
+ ${range?.join("-") ?? ""}
10281
+ ${contentHash}`),
10282
+ basis: "repository",
10283
+ kind: "file",
10284
+ entity: { type: "file", path: r.path },
10285
+ source: r.location,
10286
+ contentHash,
10287
+ summary: `${r.path}:${range?.[0] ?? "?"} ${r.kind} "${r.specifier}" \u2192 ${r.target}`,
10288
+ pointer: { kind: "file", path: r.path, ...range === void 0 ? {} : { lines: range }, commit, contentHash },
10289
+ metadata: { moduleReference: r.kind, specifier: r.specifier, typeOnly: r.typeOnly, resolvedTo: r.target }
10290
+ }, slice.value);
10291
+ }
10134
10292
 
10135
10293
  // packages/director/dist/llm/contract/evidence.js
10136
10294
  var OBSERVABLE = /* @__PURE__ */ new Set(["repository", "git", "test"]);
@@ -10154,7 +10312,7 @@ function makeClaim(ctx, input) {
10154
10312
  const basis = BASIS_ORDER.filter((b) => bases.has(b));
10155
10313
  const enforced = input.enforced ?? false;
10156
10314
  const v = input.violation;
10157
- const unverifiable = v?.language !== void 0 && ctx.baselineStructuralLanguages !== void 0 && !ctx.baselineStructuralLanguages.has(v.language);
10315
+ const unverifiable = v?.unverifiable === true || v?.language !== void 0 && ctx.baselineStructuralLanguages !== void 0 && !ctx.baselineStructuralLanguages.has(v.language);
10158
10316
  const provenance = v === void 0 || ctx.baselineKeys === void 0 || input.alignment === "ALIGNED" ? void 0 : !ctx.baselineKeys.has(v.key) ? unverifiable ? "unverified-at-adoption" : "introduced" : v.touched ? "pre-existing-touched" : "pre-existing";
10159
10317
  const preExisting = provenance === "pre-existing" || provenance === "pre-existing-touched";
10160
10318
  return {
@@ -10303,69 +10461,325 @@ function declaredReferenceParts(message2) {
10303
10461
  return m === null ? void 0 : { governing: m[1] ?? "", offending: `ref:${m[2] ?? ""}:${m[3] ?? ""}` };
10304
10462
  }
10305
10463
 
10306
- // packages/director/dist/review/rules.js
10307
- var DECISIONS = `${STATE_DIR_NAME}/decisions/`;
10308
- var PROPOSALS = `${STATE_DIR_NAME}/decisions/proposals/`;
10309
- var TEST_FILE3 = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
10310
- var decoder2 = new TextDecoder("utf-8");
10311
- function blobSource(end2) {
10312
- if (end2 === "HEAD" || end2 === "INDEX")
10313
- return end2;
10314
- return typeof end2 === "string" ? void 0 : { commit: end2.commit };
10464
+ // packages/director/dist/context/policy.js
10465
+ var CONTEXT_POLICY_VERSION = "3";
10466
+ var EDGE_WEIGHTS = {
10467
+ GOVERNS: 1,
10468
+ IMPLEMENTS: 0.9,
10469
+ TRACKED_BY: 0.9,
10470
+ VALIDATED_BY: 0.8,
10471
+ REQUIRES: 0.7,
10472
+ CALLS: 0.7,
10473
+ CONTAINS: 0.6,
10474
+ IMPORTS: 0.5,
10475
+ CHANGED_WITH: 0.3
10476
+ };
10477
+ var SEED_STRENGTH = { id: 1, path: 1, symbol: 1, "symbol-name": 0.9, keyword: 0.6, diff: 1 };
10478
+ var CROSS_LANGUAGE_FACTOR = 0.5;
10479
+ var DEFAULT_LIMITS = {
10480
+ /** Upper bound; project.yaml context.max_depth (1–3, default 2) replaces it. */
10481
+ maxDepth: 2,
10482
+ /** Finalized candidates (05 nodeLimit 200). */
10483
+ nodeLimit: 200,
10484
+ /** Edges read per expanded node. */
10485
+ edgeLimit: 2e3
10486
+ };
10487
+ var MIN_BUDGET = 1e3;
10488
+ var MAX_BUDGET = 1e6;
10489
+ var TASK_TOKEN_LIMIT = 200;
10490
+ var KEYWORD = {
10491
+ /** Keyword seeds kept (05: top 8). */
10492
+ top: 8,
10493
+ /** Keyword matches below this fraction of the best one are not seeds. */
10494
+ minFraction: 0.3,
10495
+ /** BM25 parameters. */
10496
+ k1: 1.2,
10497
+ b: 0.75
10498
+ };
10499
+ var SYMBOL_NAME_MAX_MATCHES = 5;
10500
+ var TIER_ORDER = [
10501
+ "requirement",
10502
+ "decision",
10503
+ "pending",
10504
+ "code-direct",
10505
+ "test",
10506
+ "issue",
10507
+ "code-structural",
10508
+ "code-historical"
10509
+ ];
10510
+ var TIER_SHARE = {
10511
+ requirement: 0.25,
10512
+ decision: 0.15,
10513
+ pending: 0.05,
10514
+ "code-direct": 0.3,
10515
+ test: 0.12,
10516
+ issue: 0.05,
10517
+ "code-structural": 0.06,
10518
+ "code-historical": 0.02
10519
+ };
10520
+ var L3_PER_FILE_FIRST_PASS = 2;
10521
+ var LEADING_CONTEXT_LINES = 12;
10522
+ var GENERIC_FILE_WINDOW = { lines: 40, chars: 2e3 };
10523
+
10524
+ // packages/director/dist/context/expand.js
10525
+ var HUBS = /* @__PURE__ */ new Set(["project", "milestone", "decision", "issue"]);
10526
+ function displayRef(id3) {
10527
+ const ref = parseNodeId(id3);
10528
+ if (ref === void 0)
10529
+ return id3;
10530
+ switch (ref.type) {
10531
+ case "project":
10532
+ return "project";
10533
+ case "file":
10534
+ return ref.path;
10535
+ case "symbol":
10536
+ return `${ref.path}#${ref.symbol}`;
10537
+ case "test":
10538
+ return `${ref.path}#${ref.name}`;
10539
+ default:
10540
+ return ref.id;
10541
+ }
10315
10542
  }
10316
- async function sideText(ctx, side, file) {
10317
- if (side === "WORKTREE")
10318
- return readSourceFile(ctx.root, file).value;
10319
- const source = blobSource(side);
10320
- if (source === void 0)
10321
- return void 0;
10322
- const blob = await ctx.git.readBlob(source, file);
10323
- return blob.value === void 0 ? void 0 : canonicalSourceText(decoder2.decode(blob.value));
10543
+ function better(a, b) {
10544
+ return b.score - a.score || a.depth - b.depth || compareUtf8(a.node.id, b.node.id);
10324
10545
  }
10325
- function decisionsOf(file, text4) {
10326
- if (file.endsWith(".md"))
10327
- return [...parseDefinitionMarkdown(file, text4).value?.decisions ?? []];
10328
- const d = parseDecisionFile(file, text4).value;
10329
- return d === void 0 ? [] : [d];
10546
+ var Heap = class {
10547
+ items = [];
10548
+ get size() {
10549
+ return this.items.length;
10550
+ }
10551
+ push(c) {
10552
+ const a = this.items;
10553
+ a.push(c);
10554
+ let i = a.length - 1;
10555
+ while (i > 0) {
10556
+ const p = i - 1 >> 1;
10557
+ if (better(a[i], a[p]) >= 0)
10558
+ break;
10559
+ [a[i], a[p]] = [a[p], a[i]];
10560
+ i = p;
10561
+ }
10562
+ }
10563
+ pop() {
10564
+ const a = this.items;
10565
+ const top = a[0];
10566
+ const last = a.pop();
10567
+ if (a.length > 0 && last !== void 0) {
10568
+ a[0] = last;
10569
+ let i = 0;
10570
+ for (; ; ) {
10571
+ const l = 2 * i + 1, r = l + 1;
10572
+ let m = i;
10573
+ if (l < a.length && better(a[l], a[m]) < 0)
10574
+ m = l;
10575
+ if (r < a.length && better(a[r], a[m]) < 0)
10576
+ m = r;
10577
+ if (m === i)
10578
+ break;
10579
+ [a[i], a[m]] = [a[m], a[i]];
10580
+ i = m;
10581
+ }
10582
+ }
10583
+ return top;
10584
+ }
10585
+ };
10586
+ function step(edge) {
10587
+ const provenance = typeof edge.metadata.provenance === "string" ? edge.metadata.provenance : "unknown";
10588
+ return { from: displayRef(edge.from), type: edge.type, to: displayRef(edge.to), provenance };
10330
10589
  }
10331
- var decisionFiles = (ctx) => ctx.files.filter((f) => f.path.startsWith(DECISIONS) && !f.path.startsWith(PROPOSALS));
10332
- var codeSeeds = (ctx) => ctx.seeds.filter((s) => s.entity.type === "symbol" || s.entity.type === "file");
10333
- async function decisionIntegrity(ctx) {
10334
- const out = [];
10335
- for (const f of decisionFiles(ctx)) {
10336
- const oldPath = f.oldPath ?? f.path;
10337
- const oldText = f.kind === "added" || f.kind === "untracked" ? void 0 : await sideText(ctx, ctx.from, oldPath);
10338
- const before = oldText === void 0 ? [] : decisionsOf(oldPath, oldText);
10339
- const after = f.kind === "deleted" ? [] : ctx.truth.decisions.filter((d) => d.location.path === f.path);
10340
- const ids2 = [.../* @__PURE__ */ new Set([...before.map((d) => d.id), ...after.map((d) => d.id)])].sort(compareUtf8);
10341
- for (const id3 of ids2) {
10342
- const o = before.find((d) => d.id === id3);
10343
- const n = after.find((d) => d.id === id3);
10344
- const oldEvidence = o === void 0 || oldText === void 0 ? void 0 : truthEvidenceFromText(ctx.store, { kind: "decision", id: id3, title: o.title, location: o.location }, oldText, `old:${ctx.fromLabel}`);
10345
- const newEvidence = n === void 0 ? void 0 : truthEvidence(ctx.store, ctx.reader, { kind: "decision", id: id3, title: n.title, location: n.location });
10346
- const evidence = [oldEvidence, newEvidence, ...f.evidenceIds];
10347
- const subject = { kind: "decision", id: id3 };
10348
- const wasConfirmed = o !== void 0 && (o.state === "confirmed" || o.state === "superseded");
10349
- if (n === void 0) {
10350
- if (wasConfirmed) {
10351
- const c2 = makeClaim(ctx, {
10352
- rule: "decision-integrity",
10353
- subject,
10354
- alignment: "CONFLICT",
10355
- reason: "confirmed-decision-removed",
10356
- enforced: true,
10357
- evidence,
10358
- expected: `${id3} stays in Project Truth (confirmed Decisions are superseded, not deleted)`,
10359
- observed: `${id3} was removed`
10360
- });
10361
- if (c2 !== void 0)
10362
- out.push(c2);
10363
- }
10590
+ function expandCandidates(store, seeds, options) {
10591
+ const heap = new Heap();
10592
+ const best = /* @__PURE__ */ new Map();
10593
+ const seedIds = new Set(seeds.map((s) => s.id));
10594
+ for (const s of seeds) {
10595
+ const ref = parseNodeId(s.id);
10596
+ const node = ref === void 0 ? void 0 : store.getNode(ref);
10597
+ if (node === void 0)
10598
+ continue;
10599
+ heap.push({ node, score: s.strength, depth: 0, seed: s.id, steps: [] });
10600
+ best.set(s.id, Math.max(best.get(s.id) ?? 0, s.strength));
10601
+ }
10602
+ const done = /* @__PURE__ */ new Map();
10603
+ let truncated = false;
10604
+ while (heap.size > 0) {
10605
+ const c = heap.pop();
10606
+ if (done.has(c.node.id))
10607
+ continue;
10608
+ if (done.size >= options.nodeLimit) {
10609
+ truncated = true;
10610
+ break;
10611
+ }
10612
+ done.set(c.node.id, c);
10613
+ if (c.depth >= options.maxDepth || HUBS.has(c.node.type) && !(c.depth === 0 && seedIds.has(c.node.id)))
10614
+ continue;
10615
+ const adjacent = store.adjacentEdges([c.node.ref], { direction: "both", limit: options.edgeLimit });
10616
+ if (adjacent.truncated)
10617
+ truncated = true;
10618
+ for (const edge of adjacent.edges) {
10619
+ if (edge.type === "SUPERSEDES")
10364
10620
  continue;
10365
- }
10366
- if (n.state !== "confirmed" && n.state !== "superseded")
10621
+ const other = edge.from === c.node.id ? edge.to : edge.from;
10622
+ if (done.has(other) || other.startsWith("project:"))
10367
10623
  continue;
10368
- const lock = verifyDecisionLock(n).value;
10624
+ const score = c.score * EDGE_WEIGHTS[edge.type];
10625
+ if (score <= (best.get(other) ?? 0))
10626
+ continue;
10627
+ const ref = parseNodeId(other);
10628
+ const node = ref === void 0 ? void 0 : store.getNode(ref);
10629
+ if (node === void 0)
10630
+ continue;
10631
+ best.set(other, score);
10632
+ heap.push({ node, score, depth: c.depth + 1, seed: c.seed, steps: [...c.steps, step(edge)] });
10633
+ }
10634
+ }
10635
+ return { candidates: [...done.values()].sort(better), truncated };
10636
+ }
10637
+
10638
+ // packages/director/dist/review/seeds.js
10639
+ var PROPOSALS = `${STATE_DIR_NAME}/decisions/proposals/`;
10640
+ function hunkRange(h) {
10641
+ return h.newLines > 0 ? [h.newStart, h.newStart + h.newLines - 1] : [Math.max(1, h.newStart), Math.max(1, h.newStart)];
10642
+ }
10643
+ function overlaps(loc2, range) {
10644
+ if (loc2?.startLine === void 0)
10645
+ return false;
10646
+ const end2 = loc2.endLine ?? loc2.startLine;
10647
+ return range[0] <= end2 && range[1] >= loc2.startLine;
10648
+ }
10649
+ function owned(graph, path35) {
10650
+ const out = [];
10651
+ let afterId;
10652
+ for (; ; ) {
10653
+ const page = graph.listNodes({ ownerFile: path35, limit: 1e3, ...afterId === void 0 ? {} : { afterId } });
10654
+ out.push(...page);
10655
+ if (page.length < 1e3)
10656
+ return out;
10657
+ afterId = page[page.length - 1]?.id;
10658
+ }
10659
+ }
10660
+ var width = (loc2) => (loc2?.endLine ?? 0) - (loc2?.startLine ?? 0);
10661
+ function truthDefinitions(truth) {
10662
+ return [
10663
+ ...truth.requirements.map((d) => ({ ref: definitionRef("requirement", d.id), location: d.location })),
10664
+ ...truth.decisions.map((d) => ({ ref: definitionRef("decision", d.id), location: d.location })),
10665
+ ...truth.constraints.map((d) => ({ ref: definitionRef("decision", d.id), location: d.location })),
10666
+ ...truth.issues.map((d) => ({ ref: definitionRef("issue", d.id), location: d.location })),
10667
+ ...truth.milestones.map((d) => ({ ref: definitionRef("milestone", d.id), location: d.location }))
10668
+ ];
10669
+ }
10670
+ function diffSeeds(files, graph, truth) {
10671
+ const seeds = /* @__PURE__ */ new Map();
10672
+ const put = (entity, reason, path35, evidenceId2) => {
10673
+ if (graph.getNode(entity) === void 0)
10674
+ return;
10675
+ const id3 = nodeId(entity);
10676
+ const s = seeds.get(id3) ?? { entity, reason, path: path35, evidence: /* @__PURE__ */ new Set() };
10677
+ s.evidence.add(evidenceId2);
10678
+ seeds.set(id3, s);
10679
+ };
10680
+ const defs = truthDefinitions(truth);
10681
+ for (const f of files) {
10682
+ if (f.kind === "deleted")
10683
+ continue;
10684
+ if (f.path.startsWith(`${STATE_DIR_NAME}/`)) {
10685
+ if (f.path.startsWith(PROPOSALS))
10686
+ continue;
10687
+ for (const h of f.hunks) {
10688
+ for (const d of defs)
10689
+ if (d.location.path === f.path && overlaps(d.location, hunkRange(h)))
10690
+ put(d.ref, "truth-changed", f.path, h.evidenceId);
10691
+ }
10692
+ continue;
10693
+ }
10694
+ const nodes = owned(graph, f.path).filter((n) => n.type === "symbol" || n.type === "test");
10695
+ const ranges = new Map(nodes.map((n) => [n.id, nodeLocations(n)]));
10696
+ let touched = false;
10697
+ for (const h of f.hunks) {
10698
+ const range = hunkRange(h);
10699
+ const hits = nodes.map((n) => ({ n, at: (ranges.get(n.id) ?? []).filter((l) => overlaps(l, range)) })).filter((x) => x.at.length > 0);
10700
+ const inner = hits.filter((x) => !hits.some((o) => o !== x && o.at.some((ol) => x.at.some((xl) => width(ol) < width(xl) && overlaps(xl, [ol.startLine ?? 0, ol.endLine ?? 0])))));
10701
+ for (const { n } of inner) {
10702
+ put(n.ref, "hunk-overlap", f.path, h.evidenceId);
10703
+ touched = true;
10704
+ }
10705
+ }
10706
+ if (!touched)
10707
+ for (const e of f.evidenceIds)
10708
+ put(fileRef(f.path), "file-changed", f.path, e);
10709
+ }
10710
+ return [...seeds.entries()].sort(([a], [b]) => compareUtf8(a, b)).map(([id3, s]) => ({
10711
+ id: id3,
10712
+ ref: displayRef(id3),
10713
+ entity: s.entity,
10714
+ reason: s.reason,
10715
+ path: s.path,
10716
+ evidenceIds: [...s.evidence].sort(compareUtf8)
10717
+ }));
10718
+ }
10719
+
10720
+ // packages/director/dist/review/rules.js
10721
+ var DECISIONS = `${STATE_DIR_NAME}/decisions/`;
10722
+ var PROPOSALS2 = `${STATE_DIR_NAME}/decisions/proposals/`;
10723
+ var TEST_FILE3 = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
10724
+ var decoder2 = new TextDecoder("utf-8");
10725
+ function blobSource(end2) {
10726
+ if (end2 === "HEAD" || end2 === "INDEX")
10727
+ return end2;
10728
+ return typeof end2 === "string" ? void 0 : { commit: end2.commit };
10729
+ }
10730
+ async function sideText(ctx, side, file) {
10731
+ if (side === "WORKTREE")
10732
+ return readSourceFile(ctx.root, file).value;
10733
+ const source = blobSource(side);
10734
+ if (source === void 0)
10735
+ return void 0;
10736
+ const blob = await ctx.git.readBlob(source, file);
10737
+ return blob.value === void 0 ? void 0 : canonicalSourceText(decoder2.decode(blob.value));
10738
+ }
10739
+ function decisionsOf(file, text4) {
10740
+ if (file.endsWith(".md"))
10741
+ return [...parseDefinitionMarkdown(file, text4).value?.decisions ?? []];
10742
+ const d = parseDecisionFile(file, text4).value;
10743
+ return d === void 0 ? [] : [d];
10744
+ }
10745
+ var decisionFiles = (ctx) => ctx.files.filter((f) => f.path.startsWith(DECISIONS) && !f.path.startsWith(PROPOSALS2));
10746
+ var codeSeeds = (ctx) => ctx.seeds.filter((s) => s.entity.type === "symbol" || s.entity.type === "file");
10747
+ async function decisionIntegrity(ctx) {
10748
+ const out = [];
10749
+ for (const f of decisionFiles(ctx)) {
10750
+ const oldPath = f.oldPath ?? f.path;
10751
+ const oldText = f.kind === "added" || f.kind === "untracked" ? void 0 : await sideText(ctx, ctx.from, oldPath);
10752
+ const before = oldText === void 0 ? [] : decisionsOf(oldPath, oldText);
10753
+ const after = f.kind === "deleted" ? [] : ctx.truth.decisions.filter((d) => d.location.path === f.path);
10754
+ const ids2 = [.../* @__PURE__ */ new Set([...before.map((d) => d.id), ...after.map((d) => d.id)])].sort(compareUtf8);
10755
+ for (const id3 of ids2) {
10756
+ const o = before.find((d) => d.id === id3);
10757
+ const n = after.find((d) => d.id === id3);
10758
+ const oldEvidence = o === void 0 || oldText === void 0 ? void 0 : truthEvidenceFromText(ctx.store, { kind: "decision", id: id3, title: o.title, location: o.location }, oldText, `old:${ctx.fromLabel}`);
10759
+ const newEvidence = n === void 0 ? void 0 : truthEvidence(ctx.store, ctx.reader, { kind: "decision", id: id3, title: n.title, location: n.location });
10760
+ const evidence = [oldEvidence, newEvidence, ...f.evidenceIds];
10761
+ const subject = { kind: "decision", id: id3 };
10762
+ const wasConfirmed = o !== void 0 && (o.state === "confirmed" || o.state === "superseded");
10763
+ if (n === void 0) {
10764
+ if (wasConfirmed) {
10765
+ const c2 = makeClaim(ctx, {
10766
+ rule: "decision-integrity",
10767
+ subject,
10768
+ alignment: "CONFLICT",
10769
+ reason: "confirmed-decision-removed",
10770
+ enforced: true,
10771
+ evidence,
10772
+ expected: `${id3} stays in Project Truth (confirmed Decisions are superseded, not deleted)`,
10773
+ observed: `${id3} was removed`
10774
+ });
10775
+ if (c2 !== void 0)
10776
+ out.push(c2);
10777
+ }
10778
+ continue;
10779
+ }
10780
+ if (n.state !== "confirmed" && n.state !== "superseded")
10781
+ continue;
10782
+ const lock = verifyDecisionLock(n).value;
10369
10783
  let c;
10370
10784
  if (wasConfirmed && decisionLockDigest(o) !== decisionLockDigest(n)) {
10371
10785
  c = makeClaim(ctx, {
@@ -10792,6 +11206,124 @@ function scopeRelevance(ctx, covered = /* @__PURE__ */ new Set()) {
10792
11206
  }
10793
11207
  return out;
10794
11208
  }
11209
+ var overlapsLines = (loc2, [a, b]) => {
11210
+ const start2 = loc2.startLine ?? 1;
11211
+ return start2 <= b && (loc2.endLine ?? start2) >= a;
11212
+ };
11213
+ var preview = (items) => `${items.slice(0, 5).join(", ")}${items.length > 5 ? `, \u2026 (${items.length} in all)` : ""}`;
11214
+ function confirmedAfterBaseline(confirmedAt, recordedAt) {
11215
+ if (confirmedAt === void 0 || recordedAt === void 0)
11216
+ return false;
11217
+ const c = Date.parse(confirmedAt);
11218
+ const r = Date.parse(recordedAt);
11219
+ return Number.isFinite(c) && Number.isFinite(r) && c > r;
11220
+ }
11221
+ async function decisionForbidsImport(ctx) {
11222
+ const decisions = ctx.truth.decisions.filter((d) => isActive(d) && (d.forbids.importedPaths?.length ?? 0) > 0).sort((a, b) => compareUtf8(a.id, b.id));
11223
+ if (decisions.length === 0)
11224
+ return { claims: [], limitations: [] };
11225
+ const matchers = decisions.map((d) => ({ d, match: (d.forbids.importedPaths ?? []).flatMap((p) => compileRepoPattern(p) ?? []) }));
11226
+ const changed = ctx.files.filter((f) => f.kind !== "deleted" && !f.binary && !f.path.startsWith(`${STATE_DIR_NAME}/`) && f.hunks.length > 0);
11227
+ const usable = [];
11228
+ const otherSide = [];
11229
+ for (const f of changed) {
11230
+ if (ctx.to === "WORKTREE") {
11231
+ usable.push(f);
11232
+ continue;
11233
+ }
11234
+ const side = await sideText(ctx, ctx.to, f.path);
11235
+ const current = readSourceFile(ctx.root, f.path).value;
11236
+ if (side !== void 0 && current !== void 0 && side === canonicalSourceText(current))
11237
+ usable.push(f);
11238
+ else
11239
+ otherSide.push(f.path);
11240
+ }
11241
+ const indexed = await readIndexedModuleReferences(ctx.root, usable.map((f) => f.path));
11242
+ const unresolved = [];
11243
+ const ambiguous = [];
11244
+ const unsupported = [];
11245
+ const notIndexedTarget = [];
11246
+ const unavailable2 = [];
11247
+ const groups = /* @__PURE__ */ new Map();
11248
+ usable.forEach((f, i) => {
11249
+ const r = indexed[i];
11250
+ if (r === void 0 || r.status === "unavailable") {
11251
+ unavailable2.push(f.path);
11252
+ return;
11253
+ }
11254
+ if (r.status !== "ok")
11255
+ return;
11256
+ for (const ref of r.references) {
11257
+ const hunks = f.hunks.filter((h) => overlapsLines(ref.location, hunkRange(h))).map((h) => h.evidenceId);
11258
+ if (hunks.length === 0)
11259
+ continue;
11260
+ const where = `${f.path}:${ref.location.startLine ?? "?"}`;
11261
+ const target = ref.target;
11262
+ if (target === void 0) {
11263
+ const s = ref.resolution.status;
11264
+ if (s === "unresolved")
11265
+ unresolved.push(where);
11266
+ else if (s === "ambiguous")
11267
+ ambiguous.push(where);
11268
+ else if (s === "unsupported")
11269
+ unsupported.push(where);
11270
+ else if (s === "resolved")
11271
+ notIndexedTarget.push(where);
11272
+ continue;
11273
+ }
11274
+ for (const { d, match } of matchers) {
11275
+ if (!match.some((m) => m(target)))
11276
+ continue;
11277
+ const key2 = `${d.id}
11278
+ ${f.path}
11279
+ ${target}`;
11280
+ const g = groups.get(key2) ?? { d, source: f.path, target, sites: [] };
11281
+ g.sites.push({ ref, hunks });
11282
+ groups.set(key2, g);
11283
+ }
11284
+ }
11285
+ });
11286
+ const coverage = ctx.baselineEvaluatedRules?.has("decision-forbids-import") === true;
11287
+ const claims = [];
11288
+ for (const g of [...groups.values()].sort((a, b) => compareUtf8(a.d.id, b.d.id) || compareUtf8(a.source, b.source) || compareUtf8(a.target, b.target))) {
11289
+ const where = g.sites.map((s) => `${g.source}:${s.ref.location.startLine ?? "?"} ${s.ref.kind} "${s.ref.specifier}"${s.ref.typeOnly ? " (type-only)" : ""}`).join("; ");
11290
+ const unverifiable = ctx.baselineKeys !== void 0 && !coverage && !confirmedAfterBaseline(g.d.confirmedAt, ctx.baselineRecordedAt);
11291
+ const c = makeClaim(ctx, {
11292
+ rule: "decision-forbids-import",
11293
+ subject: { kind: "decision", id: g.d.id },
11294
+ key: `import:${g.source}->${g.target}`,
11295
+ alignment: "CONFLICT",
11296
+ reason: "forbidden-import",
11297
+ enforced: g.d.enforcement === "block",
11298
+ expected: `no changed import resolves to ${(g.d.forbids.importedPaths ?? []).join(", ")} (${g.d.id})`,
11299
+ observed: `forbidden import on a changed line: ${where} resolves to ${g.target}`,
11300
+ evidence: [
11301
+ decisionEvidence(ctx, g.d.id),
11302
+ ...g.sites.map((s) => moduleReferenceEvidence(ctx.store, ctx.reader, { path: g.source, location: s.ref.location, kind: s.ref.kind, specifier: s.ref.specifier, typeOnly: s.ref.typeOnly, target: g.target }, ctx.toLabel)),
11303
+ ...g.sites.flatMap((s) => s.hunks),
11304
+ nodeEvidence(ctx, ctx.graph.getNode(fileRef(g.target)))
11305
+ ],
11306
+ violation: { key: violationKey("decision-forbids-import", g.d.id, `import:${nodeId(fileRef(g.source))}->${nodeId(fileRef(g.target))}`), touched: true, ...unverifiable ? { unverifiable: true } : {} }
11307
+ });
11308
+ if (c !== void 0)
11309
+ claims.push(c);
11310
+ }
11311
+ const limitations2 = [];
11312
+ const not = "were not checked against forbids.imported_paths";
11313
+ if (unresolved.length > 0)
11314
+ limitations2.push({ code: "imports-unresolved", message: `${unresolved.length} changed import(s) did not resolve to a repository file and ${not}: ${preview(unresolved)}.` });
11315
+ if (ambiguous.length > 0)
11316
+ limitations2.push({ code: "imports-ambiguous", message: `${ambiguous.length} changed import(s) have more than one candidate file and ${not}: ${preview(ambiguous)}.` });
11317
+ if (unsupported.length > 0)
11318
+ limitations2.push({ code: "imports-resolution-unsupported", message: `${unsupported.length} changed import(s) are in a language without module resolution (imports are recorded as written) and ${not}: ${preview(unsupported)}.` });
11319
+ if (notIndexedTarget.length > 0)
11320
+ limitations2.push({ code: "imports-target-not-indexed", message: `${notIndexedTarget.length} changed import(s) resolve to a file outside the index and ${not}: ${preview(notIndexedTarget)}.` });
11321
+ if (unavailable2.length > 0)
11322
+ limitations2.push({ code: "imports-index-unavailable", message: `The indexed imports of ${unavailable2.length} changed file(s) could not be read and ${not}: ${preview(unavailable2)}.` });
11323
+ if (otherSide.length > 0)
11324
+ limitations2.push({ code: "imports-reviewed-side-not-indexed", message: `${otherSide.length} changed file(s) differ on the reviewed side from the indexed working tree; their imports ${not}: ${preview(otherSide)}.` });
11325
+ return { claims, limitations: limitations2 };
11326
+ }
10795
11327
 
10796
11328
  // packages/director/dist/adoption/head.js
10797
11329
  var externals = (s) => s.filter((x) => x.kind === "external");
@@ -10813,7 +11345,7 @@ async function headBaselineFindings(input) {
10813
11345
  if (!found.has(key2))
10814
11346
  found.set(key2, { key: key2, rule, governing, offending, ...extra });
10815
11347
  };
10816
- const decisions = input.truth.decisions.filter((d) => isActive(d) && d.forbids.paths.length + d.forbids.symbols.length + d.forbids.dependencies.length > 0);
11348
+ const decisions = input.truth.decisions.filter((d) => isActive(d) && d.forbids.paths.length + d.forbids.symbols.length + d.forbids.dependencies.length + (d.forbids.importedPaths?.length ?? 0) > 0);
10817
11349
  const declared = input.stateDiagnostics.filter((x) => x.code === "DECLARED_SYMBOL_UNRESOLVED");
10818
11350
  const needSymbols = decisions.some((d) => d.forbids.symbols.length > 0) || declared.length > 0 && input.divergent.size > 0;
10819
11351
  const symbols = [];
@@ -10863,6 +11395,20 @@ async function headBaselineFindings(input) {
10863
11395
  }
10864
11396
  }
10865
11397
  }
11398
+ const importing = decisions.filter((d) => (d.forbids.importedPaths?.length ?? 0) > 0).map((d) => ({ d, match: (d.forbids.importedPaths ?? []).flatMap((p) => compileRepoPattern(p) ?? []) }));
11399
+ if (importing.length > 0) {
11400
+ for (const f of headFiles.filter((p) => !input.divergent.has(p))) {
11401
+ for (const e of input.graph.adjacentEdges([fileRef(f)], { direction: "outgoing", types: ["IMPORTS"], limit: 1e4 }).edges) {
11402
+ const to = parseNodeId(e.to);
11403
+ if (to?.type !== "file" || !inHead.has(to.path))
11404
+ continue;
11405
+ for (const { d, match } of importing) {
11406
+ if (match.some((m) => m(to.path)))
11407
+ add("decision-forbids-import", d.id, `import:${nodeId(fileRef(f))}->${nodeId(fileRef(to.path))}`, { path: f, enforced: d.enforcement === "block" });
11408
+ }
11409
+ }
11410
+ }
11411
+ }
10866
11412
  if (declared.length > 0 && input.divergent.size > 0)
10867
11413
  limitations2.add("declared-references-approximated-for-divergent-paths");
10868
11414
  for (const diag of declared) {
@@ -11017,7 +11563,7 @@ async function captureAdoptionBaseline(root, options) {
11017
11563
  if (options.actor.kind !== "human") {
11018
11564
  return failure([createDiagnostic("ADOPTION_FORBIDDEN", `${options.actor.kind} "${options.actor.name}" cannot capture the Adoption Baseline; adoption is a human decision`)]);
11019
11565
  }
11020
- const loaded = loadProjectTruth(root);
11566
+ const loaded = requireCompleteTruth(loadProjectTruth(root));
11021
11567
  if (loaded.value === void 0)
11022
11568
  return failure(loaded.diagnostics);
11023
11569
  const { truth } = loaded.value;
@@ -11066,7 +11612,12 @@ async function captureAdoptionBaseline(root, options) {
11066
11612
  own?.value?.dispose();
11067
11613
  }
11068
11614
  const bootstrapTruth = await bootstrapTruthOf(root, git.value);
11069
- const limitations2 = [...evaluated.limitations, ...wt.truncated ? ["working-tree-list-truncated"] : []].sort(compareUtf8);
11615
+ const evaluatedRules = ["decision-forbids", "declared-reference", "external-source-drift", ...wt.dirty ? [] : ["decision-forbids-import"]].sort(compareUtf8);
11616
+ const limitations2 = [
11617
+ ...evaluated.limitations,
11618
+ ...wt.truncated ? ["working-tree-list-truncated"] : [],
11619
+ ...wt.dirty ? ["import-relations-not-evaluated-dirty-working-tree"] : []
11620
+ ].sort(compareUtf8);
11070
11621
  const body = {
11071
11622
  format: "duo.adoption-baseline/2",
11072
11623
  project: { name: truth.config.name, rootCommits: repo.value.rootCommitOids },
@@ -11097,7 +11648,8 @@ async function captureAdoptionBaseline(root, options) {
11097
11648
  findingsAt: "HEAD",
11098
11649
  findings: evaluated.findings,
11099
11650
  limitations: limitations2,
11100
- analysis
11651
+ analysis,
11652
+ evaluatedRules
11101
11653
  };
11102
11654
  const id3 = historyRecordId("adoption", body);
11103
11655
  const existing = loadAdoptionBaseline(root);
@@ -11269,180 +11821,6 @@ function oneGroup(groups, nodes) {
11269
11821
  return g !== void 0 && nodes.length > 1 && nodes.every((n) => groups.groups.get(n.id) === g);
11270
11822
  }
11271
11823
 
11272
- // packages/director/dist/context/policy.js
11273
- var CONTEXT_POLICY_VERSION = "2";
11274
- var EDGE_WEIGHTS = {
11275
- GOVERNS: 1,
11276
- IMPLEMENTS: 0.9,
11277
- TRACKED_BY: 0.9,
11278
- VALIDATED_BY: 0.8,
11279
- REQUIRES: 0.7,
11280
- CALLS: 0.7,
11281
- CONTAINS: 0.6,
11282
- IMPORTS: 0.5,
11283
- CHANGED_WITH: 0.3
11284
- };
11285
- var SEED_STRENGTH = { id: 1, path: 1, symbol: 1, "symbol-name": 0.9, keyword: 0.6, diff: 1 };
11286
- var CROSS_LANGUAGE_FACTOR = 0.5;
11287
- var DEFAULT_LIMITS = {
11288
- /** Upper bound; project.yaml context.max_depth (1–3, default 2) replaces it. */
11289
- maxDepth: 2,
11290
- /** Finalized candidates (05 nodeLimit 200). */
11291
- nodeLimit: 200,
11292
- /** Edges read per expanded node. */
11293
- edgeLimit: 2e3
11294
- };
11295
- var MIN_BUDGET = 1e3;
11296
- var MAX_BUDGET = 1e6;
11297
- var TASK_TOKEN_LIMIT = 200;
11298
- var KEYWORD = {
11299
- /** Keyword seeds kept (05: top 8). */
11300
- top: 8,
11301
- /** Keyword matches below this fraction of the best one are not seeds. */
11302
- minFraction: 0.3,
11303
- /** BM25 parameters. */
11304
- k1: 1.2,
11305
- b: 0.75
11306
- };
11307
- var SYMBOL_NAME_MAX_MATCHES = 5;
11308
- var TIER_ORDER = [
11309
- "requirement",
11310
- "decision",
11311
- "pending",
11312
- "code-direct",
11313
- "test",
11314
- "issue",
11315
- "code-structural",
11316
- "code-historical"
11317
- ];
11318
- var TIER_SHARE = {
11319
- requirement: 0.25,
11320
- decision: 0.15,
11321
- pending: 0.05,
11322
- "code-direct": 0.3,
11323
- test: 0.12,
11324
- issue: 0.05,
11325
- "code-structural": 0.06,
11326
- "code-historical": 0.02
11327
- };
11328
- var L3_PER_FILE_FIRST_PASS = 2;
11329
- var LEADING_CONTEXT_LINES = 12;
11330
- var GENERIC_FILE_WINDOW = { lines: 40, chars: 2e3 };
11331
-
11332
- // packages/director/dist/context/expand.js
11333
- var HUBS = /* @__PURE__ */ new Set(["project", "milestone", "decision", "issue"]);
11334
- function displayRef(id3) {
11335
- const ref = parseNodeId(id3);
11336
- if (ref === void 0)
11337
- return id3;
11338
- switch (ref.type) {
11339
- case "project":
11340
- return "project";
11341
- case "file":
11342
- return ref.path;
11343
- case "symbol":
11344
- return `${ref.path}#${ref.symbol}`;
11345
- case "test":
11346
- return `${ref.path}#${ref.name}`;
11347
- default:
11348
- return ref.id;
11349
- }
11350
- }
11351
- function better(a, b) {
11352
- return b.score - a.score || a.depth - b.depth || compareUtf8(a.node.id, b.node.id);
11353
- }
11354
- var Heap = class {
11355
- items = [];
11356
- get size() {
11357
- return this.items.length;
11358
- }
11359
- push(c) {
11360
- const a = this.items;
11361
- a.push(c);
11362
- let i = a.length - 1;
11363
- while (i > 0) {
11364
- const p = i - 1 >> 1;
11365
- if (better(a[i], a[p]) >= 0)
11366
- break;
11367
- [a[i], a[p]] = [a[p], a[i]];
11368
- i = p;
11369
- }
11370
- }
11371
- pop() {
11372
- const a = this.items;
11373
- const top = a[0];
11374
- const last = a.pop();
11375
- if (a.length > 0 && last !== void 0) {
11376
- a[0] = last;
11377
- let i = 0;
11378
- for (; ; ) {
11379
- const l = 2 * i + 1, r = l + 1;
11380
- let m = i;
11381
- if (l < a.length && better(a[l], a[m]) < 0)
11382
- m = l;
11383
- if (r < a.length && better(a[r], a[m]) < 0)
11384
- m = r;
11385
- if (m === i)
11386
- break;
11387
- [a[i], a[m]] = [a[m], a[i]];
11388
- i = m;
11389
- }
11390
- }
11391
- return top;
11392
- }
11393
- };
11394
- function step(edge) {
11395
- const provenance = typeof edge.metadata.provenance === "string" ? edge.metadata.provenance : "unknown";
11396
- return { from: displayRef(edge.from), type: edge.type, to: displayRef(edge.to), provenance };
11397
- }
11398
- function expandCandidates(store, seeds, options) {
11399
- const heap = new Heap();
11400
- const best = /* @__PURE__ */ new Map();
11401
- const seedIds = new Set(seeds.map((s) => s.id));
11402
- for (const s of seeds) {
11403
- const ref = parseNodeId(s.id);
11404
- const node = ref === void 0 ? void 0 : store.getNode(ref);
11405
- if (node === void 0)
11406
- continue;
11407
- heap.push({ node, score: s.strength, depth: 0, seed: s.id, steps: [] });
11408
- best.set(s.id, Math.max(best.get(s.id) ?? 0, s.strength));
11409
- }
11410
- const done = /* @__PURE__ */ new Map();
11411
- let truncated = false;
11412
- while (heap.size > 0) {
11413
- const c = heap.pop();
11414
- if (done.has(c.node.id))
11415
- continue;
11416
- if (done.size >= options.nodeLimit) {
11417
- truncated = true;
11418
- break;
11419
- }
11420
- done.set(c.node.id, c);
11421
- if (c.depth >= options.maxDepth || HUBS.has(c.node.type) && !(c.depth === 0 && seedIds.has(c.node.id)))
11422
- continue;
11423
- const adjacent = store.adjacentEdges([c.node.ref], { direction: "both", limit: options.edgeLimit });
11424
- if (adjacent.truncated)
11425
- truncated = true;
11426
- for (const edge of adjacent.edges) {
11427
- if (edge.type === "SUPERSEDES")
11428
- continue;
11429
- const other = edge.from === c.node.id ? edge.to : edge.from;
11430
- if (done.has(other) || other.startsWith("project:"))
11431
- continue;
11432
- const score = c.score * EDGE_WEIGHTS[edge.type];
11433
- if (score <= (best.get(other) ?? 0))
11434
- continue;
11435
- const ref = parseNodeId(other);
11436
- const node = ref === void 0 ? void 0 : store.getNode(ref);
11437
- if (node === void 0)
11438
- continue;
11439
- best.set(other, score);
11440
- heap.push({ node, score, depth: c.depth + 1, seed: c.seed, steps: [...c.steps, step(edge)] });
11441
- }
11442
- }
11443
- return { candidates: [...done.values()].sort(better), truncated };
11444
- }
11445
-
11446
11824
  // packages/director/dist/context/types.js
11447
11825
  var SEED_PROVENANCE = {
11448
11826
  id: "explicit",
@@ -11612,6 +11990,7 @@ function planContext(input) {
11612
11990
  const mil = new Map(truth.milestones.map((m) => [m.id, m]));
11613
11991
  const seedIds = new Set(input.seeds.seeds.map((s) => s.id));
11614
11992
  const exactSeeds = new Set(input.seeds.seeds.filter((s) => s.match !== "keyword").map((s) => s.id));
11993
+ const idSeeds = new Set(input.seeds.seeds.filter((s) => s.match === "id").map((s) => s.id));
11615
11994
  const slice = (loc2) => {
11616
11995
  const r = reader.slice(loc2);
11617
11996
  diagnostics.push(...r.diagnostics);
@@ -11644,6 +12023,7 @@ function planContext(input) {
11644
12023
  score: c.score,
11645
12024
  depth: c.depth,
11646
12025
  mandatory: exactSeeds.has(c.node.id),
12026
+ ...c.depth === 0 && idSeeds.has(c.node.id) ? { primary: true } : {},
11647
12027
  levels: levels([["L1", head], ["L2", body === "" ? void 0 : `${head}
11648
12028
  ${body}`], ["L3", exact === void 0 ? void 0 : `${head}
11649
12029
  ${fence(exact, "md")}`]]),
@@ -11752,7 +12132,7 @@ ${fence(exact, "yaml")}`]]),
11752
12132
  case "issue": {
11753
12133
  const i = iss.get(ref.id);
11754
12134
  if (i !== void 0)
11755
- put(issueDraft(i, node, c, slice, via(c)));
12135
+ put(issueDraft(i, node, c, slice, via(c), c.depth === 0 && idSeeds.has(node.id)));
11756
12136
  break;
11757
12137
  }
11758
12138
  case "milestone": {
@@ -11908,6 +12288,7 @@ ${fence(w.text, langOf(ref.path))}`;
11908
12288
  tier: d.tier,
11909
12289
  rank: i + 1,
11910
12290
  mandatory: d.mandatory,
12291
+ ...d.primary === true ? { primary: true } : {},
11911
12292
  levels: d.levels,
11912
12293
  via: d.via,
11913
12294
  ...d.source === void 0 ? {} : { source: d.source },
@@ -11944,7 +12325,7 @@ function proposalRefs(p) {
11944
12325
  out.add(e.id);
11945
12326
  return [...out].sort(compareUtf8);
11946
12327
  }
11947
- function issueDraft(i, node, c, slice, v) {
12328
+ function issueDraft(i, node, c, slice, v, primary) {
11948
12329
  const head = `${i.id} ${i.title} (${[i.status, i.milestone].filter((x) => x !== null).join(", ")})`;
11949
12330
  const ac = i.acceptance.map((a) => `- ${a.id} ${a.text}`);
11950
12331
  const commits = Array.isArray(node.payload.commits) ? node.payload.commits.slice(0, 3).map((x) => String(x).slice(0, 12)) : [];
@@ -11957,6 +12338,7 @@ function issueDraft(i, node, c, slice, v) {
11957
12338
  score: c.score,
11958
12339
  depth: c.depth,
11959
12340
  mandatory: c.depth === 0,
12341
+ ...primary ? { primary: true } : {},
11960
12342
  levels: levels([
11961
12343
  ["L1", head],
11962
12344
  ["L2", ac.length === 0 ? void 0 : [head, ...ac].join("\n")],
@@ -12216,8 +12598,14 @@ var STATIC_LIMITATIONS = [
12216
12598
  { code: "files-by-path", message: "Files are listed by path; whole files are never included. Symbols and tests show their own source range; a file without a structural analyzer shows at most its first lines." },
12217
12599
  { code: "no-diff", message: "The working-tree diff is not part of this packet." }
12218
12600
  ];
12219
- function limitations(plan, taskTruncated, omitted, summarized) {
12601
+ function limitations(plan, taskTruncated, omitted, summarized, budget, truncatedSeeds) {
12220
12602
  const out = [...STATIC_LIMITATIONS];
12603
+ for (const t2 of truncatedSeeds) {
12604
+ out.push({
12605
+ code: "explicit-seed-truncated",
12606
+ message: t2.shown === void 0 ? `Explicit seed ${t2.ref} could not fit within the ${budget}-token budget (its full specification is ${t2.fullTokens} tokens); it is not shown.` : `Explicit seed ${t2.ref} could not fit its full specification within the ${budget}-token budget (${t2.fullTokens} tokens); the highest fitting representation (${t2.shown}) is shown.`
12607
+ });
12608
+ }
12221
12609
  if (omitted > 0)
12222
12610
  out.push({ code: "omitted", message: `omitted: ${omitted} lower-ranked candidates did not fit the budget (ids in omittedCandidates).` });
12223
12611
  if (summarized > 0)
@@ -12240,6 +12628,11 @@ function packContext(input) {
12240
12628
  const block = (text4) => meter.count(renderItemBlock(text4) + "\n");
12241
12629
  const evidence = (i) => meter.count(renderEvidenceLine(i.ref, i.via) + "\n");
12242
12630
  const itemTokens = (i, level) => level < 0 ? 0 : block(i.levels[level]?.text ?? "") + evidence(i);
12631
+ const truncatedSeed = (s, level) => ({
12632
+ ref: s.value.ref,
12633
+ fullTokens: itemTokens(s.value, s.value.levels.length - 1),
12634
+ shown: level < 0 ? void 0 : s.value.levels[level]?.level ?? "L1"
12635
+ });
12243
12636
  const pendingTokens = (p, level) => block(p.levels[level]?.text ?? "");
12244
12637
  const build = () => {
12245
12638
  const chosen = items.filter((s) => s.level >= 0);
@@ -12277,6 +12670,7 @@ function packContext(input) {
12277
12670
  const omitted = items.filter((s) => s.level < 0);
12278
12671
  const summarized = chosen.filter((s) => s.level < s.value.levels.length - 1).length;
12279
12672
  const taskTruncated = request.taskTruncated;
12673
+ const truncatedSeeds = items.filter((s) => s.value.primary === true && s.level < s.value.levels.length - 1).map((s) => truncatedSeed(s, s.level));
12280
12674
  const packet2 = {
12281
12675
  format: "duo.context-packet/1",
12282
12676
  request,
@@ -12288,7 +12682,7 @@ function packContext(input) {
12288
12682
  issues: of((i) => i.kind === "issue" || i.kind === "milestone"),
12289
12683
  pendingDecisions: pend,
12290
12684
  evidence: chosen.map((s) => ({ id: s.value.id, ref: s.value.ref, via: s.value.via })),
12291
- limitations: limitations(plan, taskTruncated, omitted.length, summarized),
12685
+ limitations: limitations(plan, taskTruncated, omitted.length, summarized, request.budget, truncatedSeeds),
12292
12686
  signals: plan.signals,
12293
12687
  omittedCandidates: omitted.map((s) => ({ id: s.value.id, ref: s.value.ref, rank: s.value.rank, tier: s.value.tier, reason: "budget" })),
12294
12688
  truncated: omitted.length > 0 || plan.traversalTruncated || taskTruncated,
@@ -12299,7 +12693,7 @@ function packContext(input) {
12299
12693
  return { packet: packet2, markdown: renderContextMarkdown(packet2) };
12300
12694
  };
12301
12695
  const frame = build();
12302
- const frameLimits = limitations(plan, request.taskTruncated, Math.max(1, items.length), Math.max(1, items.length));
12696
+ const frameLimits = limitations(plan, request.taskTruncated, Math.max(1, items.length), Math.max(1, items.length), request.budget, items.filter((s) => s.value.primary === true).map((s) => truncatedSeed(s, 0)));
12303
12697
  const reserved = meter.count(renderContextMarkdown({ ...frame.packet, limitations: frameLimits }));
12304
12698
  if (reserved > request.budget) {
12305
12699
  return failure([createDiagnostic("CONTEXT_REQUEST_INVALID", `budget ${request.budget} is smaller than the packet frame (${reserved} tokens: task, headings, pending decisions, limitations)`)]);
@@ -12334,6 +12728,12 @@ function packContext(input) {
12334
12728
  for (const s of items)
12335
12729
  if (s.value.mandatory)
12336
12730
  raise(s, s.value.tier, void 0);
12731
+ for (const s of items) {
12732
+ if (s.value.primary !== true)
12733
+ continue;
12734
+ while (raise(s, s.value.tier, void 0)) {
12735
+ }
12736
+ }
12337
12737
  const promotion = available - spent;
12338
12738
  const byTier = (tier) => tier === "pending" ? pending : items.filter((s) => s.value.tier === tier);
12339
12739
  for (const capped of [true, false]) {
@@ -12739,7 +13139,7 @@ async function compileContext(root, request, options) {
12739
13139
  const idle = (status, signals, extra = {}) => success({ status, freshness, signals, cache: { status: "off", written: false }, performance: perf(), diagnostics: canonicalDiagnostics(diagnostics), ...extra }, canonicalDiagnostics(diagnostics));
12740
13140
  if (inspected.value.status !== "current")
12741
13141
  return idle("index-required", []);
12742
- const loaded = loadProjectTruth(root);
13142
+ const loaded = requireCompleteTruth(loadProjectTruth(root));
12743
13143
  if (loaded.value === void 0)
12744
13144
  return failure(loaded.diagnostics);
12745
13145
  const { truth } = loaded.value;
@@ -14445,88 +14845,6 @@ async function externalSourceDrift(ctx) {
14445
14845
  return { claims, limitations: limitations2 };
14446
14846
  }
14447
14847
 
14448
- // packages/director/dist/review/seeds.js
14449
- var PROPOSALS2 = `${STATE_DIR_NAME}/decisions/proposals/`;
14450
- function hunkRange(h) {
14451
- return h.newLines > 0 ? [h.newStart, h.newStart + h.newLines - 1] : [Math.max(1, h.newStart), Math.max(1, h.newStart)];
14452
- }
14453
- function overlaps(loc2, range) {
14454
- if (loc2?.startLine === void 0)
14455
- return false;
14456
- const end2 = loc2.endLine ?? loc2.startLine;
14457
- return range[0] <= end2 && range[1] >= loc2.startLine;
14458
- }
14459
- function owned(graph, path35) {
14460
- const out = [];
14461
- let afterId;
14462
- for (; ; ) {
14463
- const page = graph.listNodes({ ownerFile: path35, limit: 1e3, ...afterId === void 0 ? {} : { afterId } });
14464
- out.push(...page);
14465
- if (page.length < 1e3)
14466
- return out;
14467
- afterId = page[page.length - 1]?.id;
14468
- }
14469
- }
14470
- var width = (loc2) => (loc2?.endLine ?? 0) - (loc2?.startLine ?? 0);
14471
- function truthDefinitions(truth) {
14472
- return [
14473
- ...truth.requirements.map((d) => ({ ref: definitionRef("requirement", d.id), location: d.location })),
14474
- ...truth.decisions.map((d) => ({ ref: definitionRef("decision", d.id), location: d.location })),
14475
- ...truth.constraints.map((d) => ({ ref: definitionRef("decision", d.id), location: d.location })),
14476
- ...truth.issues.map((d) => ({ ref: definitionRef("issue", d.id), location: d.location })),
14477
- ...truth.milestones.map((d) => ({ ref: definitionRef("milestone", d.id), location: d.location }))
14478
- ];
14479
- }
14480
- function diffSeeds(files, graph, truth) {
14481
- const seeds = /* @__PURE__ */ new Map();
14482
- const put = (entity, reason, path35, evidenceId2) => {
14483
- if (graph.getNode(entity) === void 0)
14484
- return;
14485
- const id3 = nodeId(entity);
14486
- const s = seeds.get(id3) ?? { entity, reason, path: path35, evidence: /* @__PURE__ */ new Set() };
14487
- s.evidence.add(evidenceId2);
14488
- seeds.set(id3, s);
14489
- };
14490
- const defs = truthDefinitions(truth);
14491
- for (const f of files) {
14492
- if (f.kind === "deleted")
14493
- continue;
14494
- if (f.path.startsWith(`${STATE_DIR_NAME}/`)) {
14495
- if (f.path.startsWith(PROPOSALS2))
14496
- continue;
14497
- for (const h of f.hunks) {
14498
- for (const d of defs)
14499
- if (d.location.path === f.path && overlaps(d.location, hunkRange(h)))
14500
- put(d.ref, "truth-changed", f.path, h.evidenceId);
14501
- }
14502
- continue;
14503
- }
14504
- const nodes = owned(graph, f.path).filter((n) => n.type === "symbol" || n.type === "test");
14505
- const ranges = new Map(nodes.map((n) => [n.id, nodeLocations(n)]));
14506
- let touched = false;
14507
- for (const h of f.hunks) {
14508
- const range = hunkRange(h);
14509
- const hits = nodes.map((n) => ({ n, at: (ranges.get(n.id) ?? []).filter((l) => overlaps(l, range)) })).filter((x) => x.at.length > 0);
14510
- const inner = hits.filter((x) => !hits.some((o) => o !== x && o.at.some((ol) => x.at.some((xl) => width(ol) < width(xl) && overlaps(xl, [ol.startLine ?? 0, ol.endLine ?? 0])))));
14511
- for (const { n } of inner) {
14512
- put(n.ref, "hunk-overlap", f.path, h.evidenceId);
14513
- touched = true;
14514
- }
14515
- }
14516
- if (!touched)
14517
- for (const e of f.evidenceIds)
14518
- put(fileRef(f.path), "file-changed", f.path, e);
14519
- }
14520
- return [...seeds.entries()].sort(([a], [b]) => compareUtf8(a, b)).map(([id3, s]) => ({
14521
- id: id3,
14522
- ref: displayRef(id3),
14523
- entity: s.entity,
14524
- reason: s.reason,
14525
- path: s.path,
14526
- evidenceIds: [...s.evidence].sort(compareUtf8)
14527
- }));
14528
- }
14529
-
14530
14848
  // packages/director/dist/review/semantic.js
14531
14849
  var ALIGNMENTS = ["ALIGNED", "PARTIAL", "CONFLICT", "UNKNOWN"];
14532
14850
  function semanticSchema(claimIds) {
@@ -14773,7 +15091,7 @@ async function reviewChanges(root, request, options) {
14773
15091
  if (inspected.value.status !== "current") {
14774
15092
  return success({ result: { format: "duo.review/1", status: "index-required", request: requestIdentity, baseline, freshness, seeds: [], verdictBasis: { blocking: [], ask: [], warn: [] }, claims: [], evidence: [], limitations: [], semanticAssist: noAssist, metrics: zeroMetrics, diagnostics: [] }, performance: perf() });
14775
15093
  }
14776
- const loaded = loadProjectTruth(root);
15094
+ const loaded = requireCompleteTruth(loadProjectTruth(root));
14777
15095
  if (loaded.value === void 0)
14778
15096
  return { diagnostics: loaded.diagnostics };
14779
15097
  const { truth } = loaded.value;
@@ -14832,17 +15150,21 @@ async function reviewChanges(root, request, options) {
14832
15150
  stateDiagnostics: readIndexState(root).state?.diagnostics ?? [],
14833
15151
  ...request.testResults === void 0 ? {} : { testResults: request.testResults },
14834
15152
  ...adoption.baseline === void 0 ? {} : { baselineKeys: new Set(adoption.baseline.findings.map((f) => f.key)) },
14835
- ...adoption.baseline === void 0 ? {} : { baselineStructuralLanguages: new Set(adoption.baseline.analysis?.structuralLanguages ?? PRE_T18_STRUCTURAL_LANGUAGES) }
15153
+ ...adoption.baseline === void 0 ? {} : { baselineStructuralLanguages: new Set(adoption.baseline.analysis?.structuralLanguages ?? PRE_T18_STRUCTURAL_LANGUAGES) },
15154
+ ...adoption.baseline?.evaluatedRules === void 0 ? {} : { baselineEvaluatedRules: new Set(adoption.baseline.evaluatedRules) },
15155
+ ...adoption.baseline === void 0 ? {} : { baselineRecordedAt: adoption.baseline.recorded.at }
14836
15156
  };
14837
15157
  const integrity = await decisionIntegrity(ctx);
14838
- const forbids = await decisionForbids(ctx);
15158
+ const forbids2 = await decisionForbids(ctx);
15159
+ const imports = await decisionForbidsImport(ctx);
14839
15160
  const drift = await externalSourceDrift(ctx);
14840
15161
  const additions = unlinkedAdditions(ctx);
14841
- const conflicted = new Set(forbids.map((c) => c.subject.id));
15162
+ const conflicted = new Set([...forbids2, ...imports.claims].map((c) => c.subject.id));
14842
15163
  const claims = [
14843
15164
  ...integrity,
14844
15165
  ...supersedeIntegrity(ctx),
14845
- ...forbids,
15166
+ ...forbids2,
15167
+ ...imports.claims,
14846
15168
  ...decisionGovernance(ctx, conflicted),
14847
15169
  ...declaredReferences(ctx),
14848
15170
  ...requirementImplementation(ctx),
@@ -14893,6 +15215,7 @@ async function reviewChanges(root, request, options) {
14893
15215
  ...limitationsOf({ identity: diff.value.identity, from: diff.value.from, to: diff.value.to, files }, reviewPacket, task !== "", taskContext?.status === "ready"),
14894
15216
  ...analysisLimitsOf(files, options.graph),
14895
15217
  ...drift.limitations,
15218
+ ...imports.limitations,
14896
15219
  ...adoption.status === "incompatible" ? [{ code: "adoption-baseline-unusable", message: "The Adoption Baseline cannot be used (" + (adoption.reason ?? "unreadable") + "); violations are not told apart from pre-existing ones." }] : []
14897
15220
  ],
14898
15221
  semanticAssist: semantic.assist,
@@ -14913,6 +15236,25 @@ async function reviewChanges(root, request, options) {
14913
15236
  return success({ result, performance: perf() }, result.diagnostics);
14914
15237
  }
14915
15238
 
15239
+ // packages/director/dist/review/provenance-label.js
15240
+ var PROVENANCE_LABELS = {
15241
+ en: {
15242
+ introduced: "not-in-adoption-baseline",
15243
+ "pre-existing": "in-adoption-baseline",
15244
+ "pre-existing-touched": "in-adoption-baseline, touched",
15245
+ "unverified-at-adoption": "baseline-unverified"
15246
+ },
15247
+ ko: {
15248
+ introduced: "\uCC44\uD0DD \uAE30\uC900\uC120\uC5D0 \uC5C6\uC74C",
15249
+ "pre-existing": "\uCC44\uD0DD \uAE30\uC900\uC120\uC5D0 \uC874\uC7AC",
15250
+ "pre-existing-touched": "\uCC44\uD0DD \uAE30\uC900\uC120\uC5D0 \uC874\uC7AC, \uC774\uBC88 \uBCC0\uACBD\uC5D0\uC11C \uC811\uCD09",
15251
+ "unverified-at-adoption": "\uCC44\uD0DD \uC2DC\uC810 \uAC80\uC99D \uBD88\uAC00"
15252
+ }
15253
+ };
15254
+ function provenanceLabel(value, locale = "en") {
15255
+ return PROVENANCE_LABELS[locale][value];
15256
+ }
15257
+
14916
15258
  // packages/director/dist/runtime/metrics.js
14917
15259
  import fs23 from "node:fs";
14918
15260
  import fsp5 from "node:fs/promises";
@@ -15410,7 +15752,7 @@ async function withGraphWriter(root, fn) {
15410
15752
  function project(root) {
15411
15753
  const loaded = loadProjectTruth(root);
15412
15754
  if (loaded.value !== void 0)
15413
- return { value: loaded.value };
15755
+ return { value: loaded.value, diagnostics: loaded.diagnostics };
15414
15756
  const missing = loaded.diagnostics.some((d) => d.code === "PROJECT_FILE_MISSING");
15415
15757
  return { outcome: missing ? { kind: "not-initialized", diagnostics: loaded.diagnostics } : { kind: "failed", diagnostics: loaded.diagnostics } };
15416
15758
  }
@@ -15433,6 +15775,7 @@ function projectStatus(root, options = {}) {
15433
15775
  if (p.value === void 0)
15434
15776
  return p.outcome;
15435
15777
  const { truth } = p.value;
15778
+ const truthErrors = truthAuthorityErrors(p.diagnostics ?? []);
15436
15779
  const inspection = await withRegistry(options.registry, (registry) => withGraphReader(root, (graph) => inspectIndex(root, { graph, registry })));
15437
15780
  const baseline = await getAdoptionBaselineStatus(root);
15438
15781
  const pending = listDecisionProposals(truth).filter((e) => e.status === "pending");
@@ -15441,12 +15784,19 @@ function projectStatus(root, options = {}) {
15441
15784
  const llm = llmPoolOf(options).forConfig(truth.config.llm);
15442
15785
  return {
15443
15786
  kind: "ok",
15444
- diagnostics: errorsOf([...inspection.diagnostics, ...baseline.diagnostics]),
15787
+ diagnostics: errorsOf([...truthErrors, ...inspection.diagnostics, ...baseline.diagnostics]),
15445
15788
  payload: {
15446
15789
  format: STATUS_FORMAT,
15447
15790
  initialized: true,
15448
15791
  project: { name: truth.config.name, vision: truth.vision?.status ?? "missing", currentMilestone: truth.config.currentMilestone },
15449
- truth: { requirements: truth.requirements.length, decisions: truth.decisions.length, constraints: truth.constraints.length, declaredGaps: truth.gaps.length },
15792
+ truth: {
15793
+ requirements: truth.requirements.length,
15794
+ decisions: truth.decisions.length,
15795
+ constraints: truth.constraints.length,
15796
+ declaredGaps: truth.gaps.length,
15797
+ // T40 (additive): loader errors that make review, context and Decision confirm refuse this Truth. Empty when Truth loaded completely.
15798
+ errors: truthErrors.map((d) => ({ code: d.code, message: d.message, ...d.source === void 0 ? {} : { path: d.source.path, ...d.source.startLine === void 0 ? {} : { line: d.source.startLine } } }))
15799
+ },
15450
15800
  index: i === void 0 ? null : {
15451
15801
  status: i.status,
15452
15802
  fullRebuildReason: i.fullRebuildReason ?? null,
@@ -15889,6 +16239,16 @@ var repoPattern = z4.string().min(1).max(4096).refine((p) => normalizeRepoPatter
15889
16239
  var definitionId = z4.string().regex(DEFINITION_ID_PATTERN, "a definition ID such as AUTH-03 or D-004");
15890
16240
  var endpoint = z4.string().min(1).max(256).refine((v) => !v.startsWith("-") && !/[\u0000-\u001f\u007f\s]/u.test(v), "HEAD, INDEX, WORKTREE or a commit / branch name");
15891
16241
  var text2 = (max) => z4.string().trim().min(1).max(max);
16242
+ var decisionId = z4.string().regex(/^D-\d+$/u, "a Decision ID such as D-004");
16243
+ var forbidPattern = z4.string().min(1).max(4096).refine((p) => compileRepoPattern(p) !== void 0, "a repository-relative pattern review can match (no absolute or .. pattern)");
16244
+ var FORBIDS_SYMBOL_MESSAGE = "forbids.symbols matches symbol qualified names only. Do not use a DUO path#symbol reference. Use a qualified-name wildcard such as 'LegacyDb' or '*LegacyDb*'.";
16245
+ var forbidSymbol = text2(500).refine((s) => !/[/\\#]/u.test(s), FORBIDS_SYMBOL_MESSAGE);
16246
+ var forbids = z4.strictObject({
16247
+ paths: z4.array(forbidPattern).max(100).optional().describe("Repository path patterns; a change to a matching file conflicts."),
16248
+ symbols: z4.array(forbidSymbol).max(100).optional().describe("Wildcard patterns over the qualified names of changed symbols ('*' any text, '?' one character), e.g. LegacyDb, LegacyDb.run, *LegacyDb*. Not a DUO path#symbol reference. Catches changes to matching symbols, not new calls or references from other symbols."),
16249
+ dependencies: z4.array(text2(500)).max(100).optional().describe("Package names; adding one to a package.json conflicts."),
16250
+ imported_paths: z4.array(forbidPattern).max(100).optional().describe("Repository path patterns (H-72). Matches changed import/module-reference statements (import, export-from, require, dynamic import, type-only included; Python imports and C++ quoted includes where DUO resolves them) that resolve exactly to a matching repository file. It does not prove the import relation is newly introduced; unresolved, ambiguous and Java/C# imports are not checked. External packages belong in dependencies.")
16251
+ }).refine((f) => (f.paths?.length ?? 0) + (f.symbols?.length ?? 0) + (f.dependencies?.length ?? 0) + (f.imported_paths?.length ?? 0) > 0, "forbids needs at least one path, symbol, dependency or imported path");
15892
16252
  var INPUT = {
15893
16253
  duo_get_status: z4.strictObject({}),
15894
16254
  duo_get_context: z4.strictObject({ task: text2(2e4), budget: z4.number().int().min(MIN_BUDGET).max(MAX_BUDGET).optional(), profile: z4.enum(["default", "review"]).optional() }),
@@ -15911,6 +16271,10 @@ var INPUT = {
15911
16271
  answer: text2(2e3),
15912
16272
  rationale: text2(4e3).optional(),
15913
16273
  governs: z4.strictObject({ requirements: z4.array(definitionId).max(100).optional(), paths: z4.array(repoPattern).max(100).optional(), symbols: z4.array(text2(500)).max(100).optional() }).optional(),
16274
+ // H-71: the proposal may carry what a Decision enforces and what it replaces; it gains no authority until a human confirms it.
16275
+ forbids: forbids.optional(),
16276
+ enforcement: z4.enum(["warn", "block"]).optional(),
16277
+ supersedes: decisionId.optional(),
15914
16278
  agent: text2(100).optional()
15915
16279
  })
15916
16280
  };
@@ -16029,7 +16393,7 @@ ${advice.join("\n")}`);
16029
16393
  const r = p;
16030
16394
  if (r.status === "index-required")
16031
16395
  return "INDEX_REQUIRED: the DUO index is not current. Run duoctl index, then review again.";
16032
- const lines4 = r.claims.filter((c) => c.alignment !== "ALIGNED").map((c) => `- ${c.alignment} ${c.rule} ${c.subject.id}: ${c.reason}${c.provenance === void 0 ? "" : ` (${c.provenance})`}${c.blockEligible ? " [blocking]" : ""}`);
16396
+ const lines4 = r.claims.filter((c) => c.alignment !== "ALIGNED").map((c) => `- ${c.alignment} ${c.rule} ${c.subject.id}: ${c.reason}${c.provenance === void 0 ? "" : ` (${provenanceLabel(c.provenance)})`}${c.blockEligible ? " [blocking]" : ""}`);
16033
16397
  const a = r.semanticAssist;
16034
16398
  const semantic = a.status === "not-requested" ? [] : [
16035
16399
  `Semantic assistance (supplemental, never blocks): ${a.status}${a.failure === void 0 ? "" : ` (${a.failure})`}`,
@@ -16082,14 +16446,17 @@ ${advice.join("\n")}`);
16082
16446
  name: "duo_propose_decision",
16083
16447
  title: "Propose a Decision",
16084
16448
  readOnly: false,
16085
- description: "Creates a Decision proposal only (decisions/proposals/P-*.yaml). Cannot confirm or reject a decision: a human does that in duoctl or the Web UI. A proposal is not confirmed intent. The agent name is an audit label, not an authentication.",
16449
+ description: `Creates a Decision proposal only (decisions/proposals/P-*.yaml). Cannot confirm or reject a decision: a human does that in duoctl or the Web UI. A proposal is not confirmed Project Truth and does not change any review verdict. forbids and enforcement: "block" have no authority until a human confirms the proposal. supersedes names a confirmed Decision to replace; that Decision is not modified until a human confirms this proposal. governs describes what the Decision concerns (for tracing and context); it does not scope forbids. Forbids are repository-wide: they apply to matching changes anywhere in the repository. forbids.symbols are wildcard patterns over the qualified names of changed symbols ('*' any text, '?' one character), e.g. LegacyDb, LegacyDb.run, *LegacyDb*; a DUO path#symbol reference such as src/db/legacy-db.ts#LegacyDb (a display handle in context output) is not a pattern and is refused. A symbol pattern catches changes to matching symbols, not new calls or references from other code. forbids.imported_paths are repository path patterns: they match changed import/module-reference statements that resolve exactly to a matching repository file; this does not prove the import relation is newly introduced, and unresolved, ambiguous or Java/C# imports are not checked. The agent name is an audit label, not an authentication.`,
16086
16450
  run: async (args, ctx) => {
16087
16451
  const op = await proposeDecision(ctx.root, args.agent ?? ctx.agentName, {
16088
16452
  title: args.title,
16089
16453
  question: args.question,
16090
16454
  answer: args.answer,
16091
16455
  ...args.rationale === void 0 ? {} : { rationale: args.rationale },
16092
- ...args.governs === void 0 ? {} : { governs: args.governs }
16456
+ ...args.governs === void 0 ? {} : { governs: args.governs },
16457
+ ...args.forbids === void 0 ? {} : { forbids: args.forbids },
16458
+ ...args.enforcement === void 0 ? {} : { enforcement: args.enforcement },
16459
+ ...args.supersedes === void 0 ? {} : { supersedes: args.supersedes }
16093
16460
  });
16094
16461
  return { op, metric: { status: op.kind === "ok" ? "proposed" : op.kind } };
16095
16462
  },
@@ -16846,7 +17213,11 @@ async function projectDoctor(rootArg, options) {
16846
17213
  let project2;
16847
17214
  if (runs("truth.project", "git.repository")) {
16848
17215
  const loaded = loadProjectTruth(root);
16849
- if (loaded.value !== void 0) {
17216
+ const authority = loaded.value === void 0 ? [] : truthAuthorityErrors(loaded.diagnostics);
17217
+ if (loaded.value !== void 0 && authority.length > 0) {
17218
+ const problems = authority.slice(0, LIST_LIMIT).map((d) => ({ code: d.code, ...d.source?.path === void 0 ? {} : { path: d.source.path } }));
17219
+ set("truth.project", "error", "invalid", { problems }, [{ id: "fix-truth", commands: [], params: { path: problems.find((p) => p.path !== void 0)?.path ?? ".duo-project/project.yaml" } }]);
17220
+ } else if (loaded.value !== void 0) {
16850
17221
  project2 = loaded.value;
16851
17222
  const t2 = project2.truth;
16852
17223
  set("truth.project", "ok", "valid", { name: t2.config.name, requirements: t2.requirements.length, decisions: t2.decisions.length, constraints: t2.constraints.length });
@@ -17271,8 +17642,8 @@ function projectProposals(root) {
17271
17642
  const service = createDecisionService({ root });
17272
17643
  const previews = {};
17273
17644
  for (const e of d.proposals.filter((x) => x.status === "pending")) {
17274
- const preview = await service.previewConfirm(e.id);
17275
- previews[e.id] = preview.value ?? { error: preview.diagnostics.map((x) => x.code) };
17645
+ const preview2 = await service.previewConfirm(e.id);
17646
+ previews[e.id] = preview2.value ?? { error: preview2.diagnostics.map((x) => x.code) };
17276
17647
  }
17277
17648
  return { kind: "ok", diagnostics: [], payload: { proposals: d.proposals, previews } };
17278
17649
  });
@@ -17429,7 +17800,8 @@ var PROPOSAL_ID = /^P-[A-Za-z0-9-]{1,64}$/u;
17429
17800
  var SCHEMAS = {
17430
17801
  context: z5.strictObject({ task: text3(2e4), budget: z5.number().int().min(MIN_BUDGET).max(MAX_BUDGET).optional() }),
17431
17802
  review: z5.strictObject({ task: text3(2e4).optional(), from: endpoint2.optional(), to: endpoint2.optional(), includeSemanticAssist: z5.boolean().optional() }),
17432
- confirm: z5.strictObject({ confirmId: z5.string().max(80) }),
17803
+ // previewDigest is required (T34.3): a UI confirm is always bound to the preview the person reviewed.
17804
+ confirm: z5.strictObject({ confirmId: z5.string().max(80), previewDigest: z5.string().min(1).max(100) }),
17433
17805
  reject: z5.strictObject({ reason: z5.string().max(2e3).optional() }),
17434
17806
  graph: z5.strictObject({ node: text3(4096), kind: z5.enum(["trace", "impact"]).default("trace"), depth: z5.coerce.number().int().min(1).max(3).default(1) }),
17435
17807
  search: z5.strictObject({ q: text3(200), limit: z5.coerce.number().int().min(1).max(50).default(20) }),
@@ -17486,7 +17858,7 @@ async function startDuoUiServer(options) {
17486
17858
  const body2 = parse3(SCHEMAS.confirm, await readJson2(req));
17487
17859
  if (body2.confirmId.trim() !== id3)
17488
17860
  throw new HttpError(400, "UI_CONFIRM_ID_MISMATCH", "type the proposal ID to confirm it");
17489
- const r2 = await service.confirm(await actor(), id3);
17861
+ const r2 = await service.confirm(await actor(), id3, { expectedDigest: body2.previewDigest });
17490
17862
  await meter("decision", r2.value === void 0 ? "failed" : "confirmed", started);
17491
17863
  return { name: "decision", body: r2.value === void 0 ? { status: "failed", diagnostics: r2.diagnostics.map((d) => ({ code: d.code, message: d.message })) } : { status: "confirmed", result: r2.value, warnings: r2.diagnostics.map((d) => ({ code: d.code, message: d.message })) } };
17492
17864
  }
@@ -17745,6 +18117,7 @@ var EN = {
17745
18117
  "status.analysis": "Analysis: {languages}",
17746
18118
  "status.baseline": "Adoption baseline: {status}{detail}",
17747
18119
  "status.pending": "Pending decisions: {n}",
18120
+ "status.truth-errors": "Truth errors: {n}. These files were not read, so the counts above leave them out. Review, context and Decision confirm refuse to run until they are fixed (DUO does not change them):",
17748
18121
  "status.llm": "LLM: {state}",
17749
18122
  "index.done": "Indexed ({mode}{reason}) \xB7 {files} files \xB7 {analyzed} parsed \xB7 {changed} changed \xB7 graph {written}",
17750
18123
  "index.required": "INDEX_REQUIRED: the index is {status}. Run duoctl index (or pass --refresh).",
@@ -17768,6 +18141,23 @@ var EN = {
17768
18141
  "decision.confirmed": "confirmed as {id} \xB7 {path}",
17769
18142
  "decision.rejected": "rejected {id}",
17770
18143
  "decision.none": "No pending decisions.",
18144
+ "decision.preview.create": "Confirm {id}: this proposal becomes a new confirmed Decision.",
18145
+ "decision.preview.in-place": "Confirm {id}: this proposed Decision becomes confirmed in place.",
18146
+ "decision.preview.lock": "Confirm {id}: this Decision is already confirmed; confirming adds its lock.",
18147
+ "decision.preview.reject": "Reject {id}: the proposal is kept as rejected; nothing becomes a Decision.",
18148
+ "decision.preview.becomes-superseded": "becomes superseded by this Decision",
18149
+ "decision.preview.not-found": "not found; confirm will fail",
18150
+ "decision.preview.stale": "yes, Project Truth changed since this proposal was made{changed}",
18151
+ "decision.preview.fresh": "no",
18152
+ "decision.preview.stale-na": "not applicable (staleness is tracked for proposals only)",
18153
+ "decision.preview.expected": "{id} (expected; the ID in the confirm result is authoritative)",
18154
+ "decision.preview.in-place-id": "{id} (confirmed in place)",
18155
+ "decision.preview.lock-id": "{id} (already confirmed; the lock is added)",
18156
+ "decision.preview.not-set": "(not set)",
18157
+ "decision.preview.none": "(none)",
18158
+ "decision.preview.forbids-scope": "repository-wide (governs does not narrow forbids)",
18159
+ "decision.index-hint": "Run duoctl index so review and context see it.",
18160
+ "decision.truth-errors": "Not listed: {n} Truth error(s). These files could not be read; confirm and reject refuse to run until they are fixed:",
17771
18161
  "stats.none": "No metrics yet.",
17772
18162
  "not-initialized": "Not a DUO project (no .duo-project/project.yaml). Run duoctl init.",
17773
18163
  "not-implemented": "'{command}' is not implemented yet ({task}).",
@@ -17937,6 +18327,7 @@ var KO = {
17937
18327
  "status.analysis": "\uBD84\uC11D \uC218\uC900: {languages}",
17938
18328
  "status.baseline": "Adoption baseline: {status}{detail}",
17939
18329
  "status.pending": "\uB300\uAE30 \uC911 decision: {n}",
18330
+ "status.truth-errors": "Truth \uC624\uB958: {n}. \uC544\uB798 \uD30C\uC77C\uC740 \uC77D\uC9C0 \uBABB\uD574 \uC704 \uAC1C\uC218\uC5D0\uC11C \uBE60\uC84C\uC2B5\uB2C8\uB2E4. \uACE0\uCE60 \uB54C\uAE4C\uC9C0 review, context, Decision \uD655\uC815\uC740 \uC2E4\uD589\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4(DUO\uB294 \uC774 \uD30C\uC77C\uC744 \uACE0\uCE58\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4):",
17940
18331
  "status.llm": "LLM: {state}",
17941
18332
  "index.done": "Index \uC644\uB8CC({mode}{reason}) \xB7 \uD30C\uC77C {files} \xB7 parse {analyzed} \xB7 \uBCC0\uACBD {changed} \xB7 graph {written}",
17942
18333
  "index.required": "INDEX_REQUIRED: index\uAC00 {status}\uC785\uB2C8\uB2E4. duoctl index\uB97C \uC2E4\uD589\uD558\uC138\uC694(\uB610\uB294 --refresh).",
@@ -17960,6 +18351,23 @@ var KO = {
17960
18351
  "decision.confirmed": "{id}\uB85C \uD655\uC815 \xB7 {path}",
17961
18352
  "decision.rejected": "{id} \uAC70\uC808",
17962
18353
  "decision.none": "\uB300\uAE30 \uC911 decision\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
18354
+ "decision.preview.create": "{id} \uD655\uC815: \uC774 proposal\uC774 \uC0C8 \uD655\uC815 Decision\uC774 \uB429\uB2C8\uB2E4.",
18355
+ "decision.preview.in-place": "{id} \uD655\uC815: \uC774 proposed Decision\uC774 \uADF8 \uC790\uB9AC\uC5D0\uC11C \uD655\uC815\uB429\uB2C8\uB2E4.",
18356
+ "decision.preview.lock": "{id} \uD655\uC815: \uC774\uBBF8 \uD655\uC815\uB41C Decision\uC774\uBA70 lock\uC744 \uCD94\uAC00\uD569\uB2C8\uB2E4.",
18357
+ "decision.preview.reject": "{id} \uAC70\uC808: proposal\uC740 rejected\uB85C \uB0A8\uACE0 Decision\uC774 \uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
18358
+ "decision.preview.becomes-superseded": "\uC774 Decision\uC5D0 \uC758\uD574 superseded\uAC00 \uB429\uB2C8\uB2E4",
18359
+ "decision.preview.not-found": "\uC5C6\uC74C, \uD655\uC815\uC774 \uC2E4\uD328\uD569\uB2C8\uB2E4",
18360
+ "decision.preview.stale": "\uC608, proposal \uC774\uD6C4 Project Truth\uAC00 \uBC14\uB00C\uC5C8\uC2B5\uB2C8\uB2E4{changed}",
18361
+ "decision.preview.fresh": "\uC544\uB2C8\uC694",
18362
+ "decision.preview.stale-na": "\uD574\uB2F9 \uC5C6\uC74C(staleness\uB294 proposal\uC5D0\uB9CC \uC801\uC6A9)",
18363
+ "decision.preview.expected": "{id} (\uC608\uC0C1\uAC12, \uC2E4\uC81C ID\uB294 \uD655\uC815 \uACB0\uACFC\uB97C \uB530\uB985\uB2C8\uB2E4)",
18364
+ "decision.preview.in-place-id": "{id} (\uADF8 \uC790\uB9AC\uC5D0\uC11C \uD655\uC815)",
18365
+ "decision.preview.lock-id": "{id} (\uC774\uBBF8 \uD655\uC815\uB428, lock \uCD94\uAC00)",
18366
+ "decision.preview.not-set": "(\uC5C6\uC74C)",
18367
+ "decision.preview.none": "(\uBE44\uC5B4 \uC788\uC74C)",
18368
+ "decision.preview.forbids-scope": "\uC800\uC7A5\uC18C \uC804\uCCB4(governs\uB294 forbids\uC758 \uBC94\uC704\uB97C \uC881\uD788\uC9C0 \uC54A\uC74C)",
18369
+ "decision.index-hint": "review\uC640 context\uC5D0 \uBC18\uC601\uD558\uB824\uBA74 duoctl index\uB97C \uC2E4\uD589\uD558\uC138\uC694.",
18370
+ "decision.truth-errors": "\uBAA9\uB85D\uC5D0 \uC5C6\uC74C: Truth \uC624\uB958 {n}\uAC1C. \uC544\uB798 \uD30C\uC77C\uC744 \uC77D\uC9C0 \uBABB\uD588\uC73C\uBA70, \uACE0\uCE60 \uB54C\uAE4C\uC9C0 \uD655\uC815\uACFC \uAC70\uC808\uC740 \uC2E4\uD589\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4:",
17963
18371
  "stats.none": "\uC9C0\uD45C\uAC00 \uC544\uC9C1 \uC5C6\uC2B5\uB2C8\uB2E4.",
17964
18372
  "not-initialized": "DUO project\uAC00 \uC544\uB2D9\uB2C8\uB2E4(.duo-project/project.yaml \uC5C6\uC74C). duoctl init\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
17965
18373
  "not-implemented": "'{command}'\uB294 \uC544\uC9C1 \uAD6C\uD604\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4({task}).",
@@ -18221,6 +18629,59 @@ function ambiguityHint(L, a) {
18221
18629
  }
18222
18630
 
18223
18631
  // apps/cli/dist/commands/decision.js
18632
+ var DECISION_ID2 = /^D-\d+$/u;
18633
+ var LABEL_WIDTH = 13;
18634
+ function show(L, value, nested = false) {
18635
+ if (value === void 0 || value === null)
18636
+ return t(L, "decision.preview.not-set");
18637
+ if (Array.isArray(value))
18638
+ return value.length === 0 ? t(L, "decision.preview.none") : value.map((v) => show(L, v, true)).join(", ");
18639
+ if (typeof value === "object") {
18640
+ const parts = Object.entries(value).filter(([, v]) => v !== void 0 && v !== null && !(Array.isArray(v) && v.length === 0)).map(([k, v]) => `${k}: ${show(L, v, true)}`);
18641
+ if (parts.length === 0)
18642
+ return t(L, "decision.preview.none");
18643
+ return nested ? `{${parts.join("; ")}}` : parts.join("; ");
18644
+ }
18645
+ return String(value).replace(/\r?\n/gu, `
18646
+ ${" ".repeat(LABEL_WIDTH + 3)}`);
18647
+ }
18648
+ function hasForbids(forbids2) {
18649
+ return typeof forbids2 === "object" && forbids2 !== null && Object.values(forbids2).some((v) => Array.isArray(v) && v.length > 0);
18650
+ }
18651
+ function previewLines(L, p, op) {
18652
+ const c = p.candidate;
18653
+ const id3 = p.sourceId;
18654
+ const head = op === "reject" ? t(L, "decision.preview.reject", { id: id3 }) : p.action === "create" ? t(L, "decision.preview.create", { id: id3 }) : p.action === "add-lock" ? t(L, "decision.preview.lock", { id: id3 }) : t(L, "decision.preview.in-place", { id: id3 });
18655
+ const row = (label, value) => ` ${label.padEnd(LABEL_WIDTH)} ${value}`;
18656
+ const supersedes = c.supersedes === void 0 ? t(L, "decision.preview.not-set") : p.supersedes === void 0 ? `${show(L, c.supersedes)} (${t(L, "decision.preview.not-found")})` : `${p.supersedes.id} "${p.supersedes.title}" (${p.supersedes.state})${op === "confirm" ? ` \u2192 ${t(L, "decision.preview.becomes-superseded")}` : ""}`;
18657
+ const stale = p.sourceKind === "decision" ? t(L, "decision.preview.stale-na") : p.stale === void 0 ? t(L, "decision.preview.fresh") : t(L, "decision.preview.stale", { changed: p.stale.changedRefs.length > 0 ? ` (changed: ${p.stale.changedRefs.join(", ")})` : "" });
18658
+ const lines4 = [
18659
+ head,
18660
+ row("Title", show(L, c.title)),
18661
+ row("Question", show(L, c.question)),
18662
+ row("Answer", show(L, c.answer)),
18663
+ row("Kind", show(L, c.kind)),
18664
+ row("Rationale", show(L, c.rationale)),
18665
+ row("Governs", show(L, c.governs)),
18666
+ row("Forbids", show(L, c.forbids)),
18667
+ // T40 (N3): governs does not scope forbids; say so wherever forbids are set.
18668
+ ...hasForbids(c.forbids) ? [row("Forbids scope", t(L, "decision.preview.forbids-scope"))] : [],
18669
+ row("Enforcement", show(L, c.enforcement)),
18670
+ row("Supersedes", supersedes),
18671
+ // H-71: the proposer as the proposal file records it (an audit label); a YAML Decision has none to show.
18672
+ ...p.sourceKind === "proposal" ? [row("Proposed by", p.proposedBy === void 0 ? t(L, "decision.preview.not-set") : `${p.proposedBy}${p.proposedByKind === void 0 ? "" : ` (${p.proposedByKind})`}`)] : [],
18673
+ ...c.evidence === void 0 ? [] : [row("Evidence", show(L, c.evidence))],
18674
+ ...c.source === void 0 ? [] : [row("Source", show(L, c.source))],
18675
+ ...c.extensions === void 0 ? [] : [row("Extensions", show(L, c.extensions))],
18676
+ row("Stale", stale)
18677
+ ];
18678
+ if (op === "confirm") {
18679
+ const expected = p.action === "create" ? t(L, "decision.preview.expected", { id: p.expectedDecisionId }) : p.action === "add-lock" ? t(L, "decision.preview.lock-id", { id: p.expectedDecisionId }) : t(L, "decision.preview.in-place-id", { id: p.expectedDecisionId });
18680
+ lines4.push(row("Expected ID", expected));
18681
+ }
18682
+ lines4.push(row("File", p.sourcePath));
18683
+ return lines4;
18684
+ }
18224
18685
  async function decisionCommand(env, sub, id3, reason) {
18225
18686
  const project2 = requireProject(env, "decision");
18226
18687
  if (project2.value === void 0)
@@ -18230,8 +18691,12 @@ async function decisionCommand(env, sub, id3, reason) {
18230
18691
  if (sub === "list" || sub === void 0) {
18231
18692
  const pending = entries.filter((e) => e.status === "pending");
18232
18693
  const proposedDecisions = truth.decisions.filter((d) => d.state === "proposed");
18233
- const human = pending.length + proposedDecisions.length === 0 ? [t(env.locale, "decision.none")] : [...pending.map((e) => `${e.id} ${e.proposal.title} (${e.proposal.question} = ${e.proposal.answer})`), ...proposedDecisions.map((d) => `${d.id} ${d.title} (proposed Decision)`)];
18234
- return { command: "decision", exitCode: EXIT.OK, diagnostics: [], human, result: { pending: pending.map((e) => ({ id: e.id, title: e.proposal.title, question: e.proposal.question, answer: e.proposal.answer })), proposedDecisions: proposedDecisions.map((d) => ({ id: d.id, title: d.title })) } };
18694
+ const unread = truthAuthorityErrors(project2.diagnostics);
18695
+ const human = [
18696
+ ...pending.length + proposedDecisions.length === 0 ? [t(env.locale, "decision.none")] : [...pending.map((e) => `${e.id} ${e.proposal.title} (${e.proposal.question} = ${e.proposal.answer})`), ...proposedDecisions.map((d) => `${d.id} ${d.title} (proposed Decision)`)],
18697
+ ...unread.length === 0 ? [] : [t(env.locale, "decision.truth-errors", { n: unread.length }), ...unread.map((d) => ` ${d.code} ${d.source?.path ?? ""}${d.source?.startLine === void 0 ? "" : `:${d.source.startLine}`} ${d.message}`)]
18698
+ ];
18699
+ return { command: "decision", exitCode: EXIT.OK, diagnostics: unread, human, result: { pending: pending.map((e) => ({ id: e.id, title: e.proposal.title, question: e.proposal.question, answer: e.proposal.answer })), proposedDecisions: proposedDecisions.map((d) => ({ id: d.id, title: d.title })) } };
18235
18700
  }
18236
18701
  if (sub !== "confirm" && sub !== "reject")
18237
18702
  return usage("decision", "usage: duoctl decision list | confirm <id> | reject <id> [--reason <text>]");
@@ -18240,27 +18705,31 @@ async function decisionCommand(env, sub, id3, reason) {
18240
18705
  if (!env.io.isTTY || env.nonInteractive) {
18241
18706
  return failed4("decision", EXIT.ERROR, [createDiagnostic("CLI_TTY_REQUIRED", t("en", "decision.tty"))], [t(env.locale, "decision.tty")]);
18242
18707
  }
18243
- const proposal = entries.find((e) => e.id === id3);
18244
- const decision = truth.decisions.find((d) => d.id === id3);
18245
- const shown = proposal?.proposal ?? decision;
18246
- if (shown !== void 0) {
18247
- env.io.err(`${id3} "${shown.title}"`);
18248
- env.io.err(` question ${shown.question}`);
18249
- env.io.err(` answer ${shown.answer}`);
18250
- if (shown.governs.requirements.length > 0)
18251
- env.io.err(` governs ${shown.governs.requirements.join(", ")}`);
18708
+ const service = createDecisionService({ root: env.root, clock: () => env.io.now() });
18709
+ const previewed = sub === "reject" && DECISION_ID2.test(id3) ? void 0 : await service.previewConfirm(id3);
18710
+ if (previewed !== void 0) {
18711
+ if (previewed.value === void 0)
18712
+ return failed4("decision", EXIT.ERROR, previewed.diagnostics, diagLines(previewed.diagnostics), null, { status: "failed" });
18713
+ for (const line of previewLines(env.locale, previewed.value, sub))
18714
+ env.io.err(line);
18252
18715
  }
18253
18716
  const typed = (await env.io.prompt(t(env.locale, "decision.retype", { op: sub })))?.trim();
18254
18717
  if (typed !== id3)
18255
18718
  return failed4("decision", EXIT.ERROR, [], [t(env.locale, "decision.mismatch")]);
18256
- const service = createDecisionService({ root: env.root, clock: () => env.io.now() });
18257
18719
  const actor = await humanActor(env.root);
18258
18720
  if (sub === "confirm") {
18259
- const r2 = await service.confirm(actor, id3);
18721
+ const r2 = await service.confirm(actor, id3, previewed?.value === void 0 ? {} : { expectedDigest: previewed.value.digest });
18260
18722
  if (r2.value === void 0)
18261
18723
  return failed4("decision", EXIT.ERROR, r2.diagnostics, diagLines(r2.diagnostics), null, { status: "failed" });
18262
18724
  const warnings = r2.diagnostics.filter((d) => d.severity === "warning").map((d) => `${d.code}: ${d.message}`);
18263
- return { command: "decision", exitCode: EXIT.OK, diagnostics: r2.diagnostics, result: r2.value, human: [...warnings, t(env.locale, "decision.confirmed", { id: r2.value.decisionId, path: r2.value.path })], metric: { status: "confirmed" } };
18725
+ return {
18726
+ command: "decision",
18727
+ exitCode: EXIT.OK,
18728
+ diagnostics: r2.diagnostics,
18729
+ result: r2.value,
18730
+ human: [...warnings, t(env.locale, "decision.confirmed", { id: r2.value.decisionId, path: r2.value.path }), t(env.locale, "decision.index-hint")],
18731
+ metric: { status: "confirmed" }
18732
+ };
18264
18733
  }
18265
18734
  const r = await service.reject(actor, id3, reason);
18266
18735
  if (r.value === void 0)
@@ -18782,7 +19251,7 @@ function renderReview(env, r) {
18782
19251
  const ev = new Map(r.evidence.map((e) => [e.id, e]));
18783
19252
  const shown = r.claims.filter((c) => env.verbose || c.alignment !== "ALIGNED");
18784
19253
  for (const c of shown) {
18785
- const tags = [c.blockEligible ? "blocking" : "", c.provenance ?? "", c.drift ? "drift" : ""].filter((x) => x !== "").join(", ");
19254
+ const tags = [c.blockEligible ? "blocking" : "", c.provenance === void 0 ? "" : provenanceLabel(c.provenance, L), c.drift ? "drift" : ""].filter((x) => x !== "").join(", ");
18786
19255
  out.push(` ${c.alignment.padEnd(9)} ${c.rule.padEnd(26)} ${c.subject.id} \xB7 ${c.reason}${tags === "" ? "" : ` [${tags}]`}`);
18787
19256
  const pointers = c.evidenceIds.map((id3) => ev.get(id3)?.pointer).filter((p) => p?.path !== void 0).slice(0, 3).map((p) => `${p?.path ?? ""}${p?.lines === void 0 ? "" : `:${p.lines[0]}-${p.lines[1]}`}`);
18788
19257
  if (pointers.length > 0)
@@ -18941,6 +19410,11 @@ async function statusCommand(env) {
18941
19410
  const human = [
18942
19411
  t(L, "status.project", { name: s.project.name, goal: s.project.vision, milestone: s.project.currentMilestone ?? "-" }),
18943
19412
  t(L, "status.truth", { requirements: s.truth.requirements, decisions: s.truth.decisions, constraints: s.truth.constraints, gaps: s.truth.declaredGaps }),
19413
+ // T40 (N1): what the loader left out, so a lower Decision count is never silent.
19414
+ ...(s.truth.errors ?? []).length === 0 ? [] : [
19415
+ t(L, "status.truth-errors", { n: s.truth.errors?.length ?? 0 }),
19416
+ ...(s.truth.errors ?? []).map((e) => ` ${e.code} ${e.path ?? ""}${e.line === void 0 ? "" : `:${e.line}`} ${e.message}`)
19417
+ ],
18944
19418
  t(L, "status.index", { status: s.index?.status ?? "unknown", reason: s.index?.fullRebuildReason == null ? "" : ` (full rebuild: ${s.index.fullRebuildReason})` }),
18945
19419
  ...indexNeedsRun ? [t(L, "status.index-remediation")] : [],
18946
19420
  ...c === void 0 ? [] : [t(L, "status.changes", { files: c.files, analysis: c.analysisStale, modules: c.modulesToResolve, calls: c.callsMaybeRecomputed })],