@duo-director/cli 0.2.2 → 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.
@@ -157,6 +157,8 @@ 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",
162
164
  /** T40 (N1): an authoritative operation refused partial Project Truth; the loader errors follow it. */
@@ -300,6 +302,7 @@ var DIAGNOSTIC_PERSISTENCE = {
300
302
  DECISION_TARGET_UNSUPPORTED: T,
301
303
  DECISION_SUPERSEDE_TARGET_INVALID: T,
302
304
  DECISION_LOCK_BUSY: T,
305
+ DECISION_CONFIRM_PREVIEW_CHANGED: T,
303
306
  PROJECT_TRUTH_INVALID: T,
304
307
  DECISION_LOCK_MISMATCH: P,
305
308
  SOURCE_LOCATION_INVALID: P,
@@ -1314,7 +1317,8 @@ var decisionShape = {
1314
1317
  rationale: text().optional(),
1315
1318
  owner: z.literal("human").optional(),
1316
1319
  governs: z.strictObject({ requirements: ids().optional(), paths: texts().optional(), symbols: texts().optional() }).optional(),
1317
- 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(),
1318
1322
  supersedes: id().nullable().optional(),
1319
1323
  evidence: z.array(EvidencePointerSchema).optional(),
1320
1324
  source: sources,
@@ -1595,7 +1599,9 @@ function mapDecisionContent(d, ctx, governsRelation) {
1595
1599
  forbids: {
1596
1600
  dependencies: d.forbids?.dependencies ?? [],
1597
1601
  symbols: d.forbids?.symbols ?? [],
1598
- 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) } : {}
1599
1605
  },
1600
1606
  supersedes: d.supersedes ?? null,
1601
1607
  evidence: ctx.evidence(["evidence"], d.evidence),
@@ -2219,6 +2225,9 @@ function canonical(value) {
2219
2225
  }
2220
2226
  return value;
2221
2227
  }
2228
+ function candidateDigest(value) {
2229
+ return sha256(stableJson(value));
2230
+ }
2222
2231
  function stableJson(value) {
2223
2232
  return JSON.stringify(canonical(value));
2224
2233
  }
@@ -2382,6 +2391,7 @@ var DECISION_PERMISSIONS = {
2382
2391
  confirm: ["human"],
2383
2392
  reject: ["human"]
2384
2393
  };
2394
+ var CANDIDATE_FIELDS = ["title", "kind", "question", "answer", "rationale", "governs", "forbids", "enforcement", "supersedes", "evidence", "source", "extensions"];
2385
2395
  var SYSTEM = { kind: "system", name: "duo" };
2386
2396
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2387
2397
  var idTokens = (message2) => message2.split(/[^A-Za-z0-9-]+/u);
@@ -2609,7 +2619,7 @@ function createDecisionService(options) {
2609
2619
  return success({ proposalId: created.value.id, path: created.value.path, indexRequired: false, repaired }, prepared.diagnostics);
2610
2620
  });
2611
2621
  },
2612
- async confirm(actor, id3) {
2622
+ async confirm(actor, id3, confirmOptions = {}) {
2613
2623
  const denied = forbidden(actor, "confirm");
2614
2624
  if (denied.length > 0)
2615
2625
  return failure(denied);
@@ -2621,7 +2631,13 @@ function createDecisionService(options) {
2621
2631
  return failure(prepared.diagnostics);
2622
2632
  const { truth, repaired } = prepared.value;
2623
2633
  const warnings = [...prepared.diagnostics];
2624
- 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);
2625
2641
  });
2626
2642
  },
2627
2643
  async reject(actor, proposalId, reason) {
@@ -2656,39 +2672,70 @@ function createDecisionService(options) {
2656
2672
  return prepared.value === void 0 ? failure(prepared.diagnostics) : success({ repaired: prepared.value.repaired }, prepared.diagnostics);
2657
2673
  });
2658
2674
  },
2659
- async previewConfirm(proposalId) {
2660
- if (!PROPOSAL_ID_PATTERN.test(proposalId))
2661
- 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`)]);
2662
2678
  const loaded = load();
2663
2679
  if (loaded.value === void 0)
2664
2680
  return failure(loaded.diagnostics);
2665
2681
  const truth = loaded.value;
2666
- const entry = listDecisionProposals(truth).find((p) => p.id === proposalId);
2667
- if (entry === void 0)
2668
- return failure([createDiagnostic("PROPOSAL_NOT_FOUND", `No proposal ${proposalId}`)]);
2669
- if (entry.status !== "pending")
2670
- return failure([createDiagnostic("PROPOSAL_NOT_PENDING", `${proposalId} is ${entry.status}`, entry.proposal.location)]);
2671
- const stale = staleness(truth, entry.proposal);
2672
- 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);
2673
2687
  return success({
2674
- proposalId,
2675
- nextDecisionId: await allocate("D", truth),
2676
- ...stale === void 0 ? {} : { stale },
2677
- ...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
2678
2701
  });
2679
2702
  },
2680
- verifyLock(decisionId) {
2703
+ verifyLock(decisionId2) {
2681
2704
  const loaded = load();
2682
2705
  if (loaded.value === void 0)
2683
2706
  return failure(loaded.diagnostics);
2684
- const d = loaded.value.decisions.find((x) => x.id === decisionId);
2707
+ const d = loaded.value.decisions.find((x) => x.id === decisionId2);
2685
2708
  if (d === void 0)
2686
- return failure([createDiagnostic("PROPOSAL_NOT_FOUND", `No Decision ${decisionId}`)]);
2709
+ return failure([createDiagnostic("PROPOSAL_NOT_FOUND", `No Decision ${decisionId2}`)]);
2687
2710
  const r = verifyDecisionLock(d);
2688
2711
  return success(r.value, r.diagnostics);
2689
2712
  }
2690
2713
  };
2691
- 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
+ }
2692
2739
  const done = truth.decisions.find((d) => d.proposalId === id3);
2693
2740
  if (done !== void 0)
2694
2741
  return failure([createDiagnostic("PROPOSAL_NOT_PENDING", `${id3} was already confirmed as ${done.id}`)]);
@@ -2697,16 +2744,54 @@ function createDecisionService(options) {
2697
2744
  return failure([createDiagnostic("PROPOSAL_NOT_FOUND", `No proposal ${id3}`)]);
2698
2745
  if (entry.status !== "pending")
2699
2746
  return failure([createDiagnostic("PROPOSAL_NOT_PENDING", `${id3} is ${entry.status}`, entry.proposal.location)]);
2700
- const proposal = entry.proposal;
2701
- 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;
2702
2790
  if (stale !== void 0) {
2703
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));
2704
2792
  }
2705
- const text4 = await fs28.readText(`${root}/${proposal.location.path}`);
2706
- if (text4 === void 0)
2707
- return failure([createDiagnostic("PROPOSAL_NOT_FOUND", `${proposal.location.path} is missing`)]);
2708
- const data = parseYaml({ path: proposal.location.path, text: text4 }).value?.data ?? {};
2709
- 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;
2710
2795
  if (proposal.supersedes !== null) {
2711
2796
  if (target === void 0)
2712
2797
  return failure([createDiagnostic("BROKEN_REFERENCE", `${id3} supersedes ${proposal.supersedes}, which does not exist`, proposal.location)]);
@@ -2716,8 +2801,8 @@ function createDecisionService(options) {
2716
2801
  return failure([createDiagnostic("DECISION_SUPERSEDE_TARGET_INVALID", `${target.id} is ${target.state}; only a confirmed Decision can be superseded`, target.location)]);
2717
2802
  }
2718
2803
  const confirmedAt = now();
2719
- const content = (decisionId, digest) => yaml(defined({
2720
- id: decisionId,
2804
+ const content = (decisionId2, digest) => yaml(defined({
2805
+ id: decisionId2,
2721
2806
  title: data.title,
2722
2807
  kind: data.kind,
2723
2808
  state: CONFIRMED,
@@ -2740,9 +2825,9 @@ function createDecisionService(options) {
2740
2825
  confirmed_by: actor.name,
2741
2826
  lock: digest === void 0 ? void 0 : { digest }
2742
2827
  }));
2743
- const render = (decisionId) => {
2744
- const draft = parseDecisionFile(decisionPath(decisionId), content(decisionId));
2745
- 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));
2746
2831
  };
2747
2832
  const probeId = await allocate("D", truth);
2748
2833
  const probe = parseDecisionFile(decisionPath(probeId), render(probeId));
@@ -2775,17 +2860,9 @@ function createDecisionService(options) {
2775
2860
  repaired
2776
2861
  }, [...warnings, ...after.map((d) => ({ ...d, severity: "warning" }))]);
2777
2862
  }
2778
- async function confirmDecision(actor, id3, truth, repaired, warnings) {
2779
- const d = truth.decisions.find((x) => x.id === id3);
2780
- if (d === void 0)
2781
- return failure([createDiagnostic("PROPOSAL_NOT_FOUND", `No Decision ${id3}`)]);
2782
- if (!isServiceFile(d))
2783
- return failure([createDiagnostic("DECISION_TARGET_UNSUPPORTED", `${id3} is not a decisions/D-###.yaml file; DUO does not rewrite it`, d.location)]);
2784
- if (d.state === "superseded" || d.state === CONFIRMED && d.lock !== void 0) {
2785
- return failure([createDiagnostic("DECISION_LOCKED", `${id3} is ${d.state} and locked; supersede it with a new proposal`, d.location)]);
2786
- }
2787
- if (d.state === "rejected")
2788
- 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;
2789
2866
  const digest = decisionLockDigest(d);
2790
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 } };
2791
2868
  const target = d.supersedes === null ? void 0 : truth.decisions.find((t2) => t2.id === d.supersedes);
@@ -9554,6 +9631,45 @@ function sameFingerprint(a, b) {
9554
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;
9555
9632
  }
9556
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
+
9557
9673
  // packages/graph/dist/query/impact.js
9558
9674
  var RANK = { direct: 0, structural: 1, historical: 2 };
9559
9675
  var STEPS = {
@@ -10152,6 +10268,27 @@ function testRunEvidence(store, run2, test) {
10152
10268
  metadata: { status, ...run2.command === void 0 ? {} : { command: run2.command } }
10153
10269
  });
10154
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
+ }
10155
10292
 
10156
10293
  // packages/director/dist/llm/contract/evidence.js
10157
10294
  var OBSERVABLE = /* @__PURE__ */ new Set(["repository", "git", "test"]);
@@ -10175,7 +10312,7 @@ function makeClaim(ctx, input) {
10175
10312
  const basis = BASIS_ORDER.filter((b) => bases.has(b));
10176
10313
  const enforced = input.enforced ?? false;
10177
10314
  const v = input.violation;
10178
- 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);
10179
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";
10180
10317
  const preExisting = provenance === "pre-existing" || provenance === "pre-existing-touched";
10181
10318
  return {
@@ -10324,77 +10461,333 @@ function declaredReferenceParts(message2) {
10324
10461
  return m === null ? void 0 : { governing: m[1] ?? "", offending: `ref:${m[2] ?? ""}:${m[3] ?? ""}` };
10325
10462
  }
10326
10463
 
10327
- // packages/director/dist/review/rules.js
10328
- var DECISIONS = `${STATE_DIR_NAME}/decisions/`;
10329
- var PROPOSALS = `${STATE_DIR_NAME}/decisions/proposals/`;
10330
- var TEST_FILE3 = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
10331
- var decoder2 = new TextDecoder("utf-8");
10332
- function blobSource(end2) {
10333
- if (end2 === "HEAD" || end2 === "INDEX")
10334
- return end2;
10335
- 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
+ }
10336
10542
  }
10337
- async function sideText(ctx, side, file) {
10338
- if (side === "WORKTREE")
10339
- return readSourceFile(ctx.root, file).value;
10340
- const source = blobSource(side);
10341
- if (source === void 0)
10342
- return void 0;
10343
- const blob = await ctx.git.readBlob(source, file);
10344
- 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);
10345
10545
  }
10346
- function decisionsOf(file, text4) {
10347
- if (file.endsWith(".md"))
10348
- return [...parseDefinitionMarkdown(file, text4).value?.decisions ?? []];
10349
- const d = parseDecisionFile(file, text4).value;
10350
- 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 };
10351
10589
  }
10352
- var decisionFiles = (ctx) => ctx.files.filter((f) => f.path.startsWith(DECISIONS) && !f.path.startsWith(PROPOSALS));
10353
- var codeSeeds = (ctx) => ctx.seeds.filter((s) => s.entity.type === "symbol" || s.entity.type === "file");
10354
- async function decisionIntegrity(ctx) {
10355
- const out = [];
10356
- for (const f of decisionFiles(ctx)) {
10357
- const oldPath = f.oldPath ?? f.path;
10358
- const oldText = f.kind === "added" || f.kind === "untracked" ? void 0 : await sideText(ctx, ctx.from, oldPath);
10359
- const before = oldText === void 0 ? [] : decisionsOf(oldPath, oldText);
10360
- const after = f.kind === "deleted" ? [] : ctx.truth.decisions.filter((d) => d.location.path === f.path);
10361
- const ids2 = [.../* @__PURE__ */ new Set([...before.map((d) => d.id), ...after.map((d) => d.id)])].sort(compareUtf8);
10362
- for (const id3 of ids2) {
10363
- const o = before.find((d) => d.id === id3);
10364
- const n = after.find((d) => d.id === id3);
10365
- 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}`);
10366
- const newEvidence = n === void 0 ? void 0 : truthEvidence(ctx.store, ctx.reader, { kind: "decision", id: id3, title: n.title, location: n.location });
10367
- const evidence = [oldEvidence, newEvidence, ...f.evidenceIds];
10368
- const subject = { kind: "decision", id: id3 };
10369
- const wasConfirmed = o !== void 0 && (o.state === "confirmed" || o.state === "superseded");
10370
- if (n === void 0) {
10371
- if (wasConfirmed) {
10372
- const c2 = makeClaim(ctx, {
10373
- rule: "decision-integrity",
10374
- subject,
10375
- alignment: "CONFLICT",
10376
- reason: "confirmed-decision-removed",
10377
- enforced: true,
10378
- evidence,
10379
- expected: `${id3} stays in Project Truth (confirmed Decisions are superseded, not deleted)`,
10380
- observed: `${id3} was removed`
10381
- });
10382
- if (c2 !== void 0)
10383
- out.push(c2);
10384
- }
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")
10385
10620
  continue;
10386
- }
10387
- 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:"))
10388
10623
  continue;
10389
- const lock = verifyDecisionLock(n).value;
10390
- let c;
10391
- if (wasConfirmed && decisionLockDigest(o) !== decisionLockDigest(n)) {
10392
- c = makeClaim(ctx, {
10393
- rule: "decision-integrity",
10394
- subject,
10395
- alignment: "CONFLICT",
10396
- reason: "confirmed-content-changed",
10397
- enforced: true,
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;
10783
+ let c;
10784
+ if (wasConfirmed && decisionLockDigest(o) !== decisionLockDigest(n)) {
10785
+ c = makeClaim(ctx, {
10786
+ rule: "decision-integrity",
10787
+ subject,
10788
+ alignment: "CONFLICT",
10789
+ reason: "confirmed-content-changed",
10790
+ enforced: true,
10398
10791
  evidence,
10399
10792
  expected: `confirmed ${id3} content unchanged (supersede it with a new Decision instead)`,
10400
10793
  observed: `content fields of ${id3} changed`
@@ -10813,6 +11206,124 @@ function scopeRelevance(ctx, covered = /* @__PURE__ */ new Set()) {
10813
11206
  }
10814
11207
  return out;
10815
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
+ }
10816
11327
 
10817
11328
  // packages/director/dist/adoption/head.js
10818
11329
  var externals = (s) => s.filter((x) => x.kind === "external");
@@ -10834,7 +11345,7 @@ async function headBaselineFindings(input) {
10834
11345
  if (!found.has(key2))
10835
11346
  found.set(key2, { key: key2, rule, governing, offending, ...extra });
10836
11347
  };
10837
- 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);
10838
11349
  const declared = input.stateDiagnostics.filter((x) => x.code === "DECLARED_SYMBOL_UNRESOLVED");
10839
11350
  const needSymbols = decisions.some((d) => d.forbids.symbols.length > 0) || declared.length > 0 && input.divergent.size > 0;
10840
11351
  const symbols = [];
@@ -10884,6 +11395,20 @@ async function headBaselineFindings(input) {
10884
11395
  }
10885
11396
  }
10886
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
+ }
10887
11412
  if (declared.length > 0 && input.divergent.size > 0)
10888
11413
  limitations2.add("declared-references-approximated-for-divergent-paths");
10889
11414
  for (const diag of declared) {
@@ -11087,7 +11612,12 @@ async function captureAdoptionBaseline(root, options) {
11087
11612
  own?.value?.dispose();
11088
11613
  }
11089
11614
  const bootstrapTruth = await bootstrapTruthOf(root, git.value);
11090
- 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);
11091
11621
  const body = {
11092
11622
  format: "duo.adoption-baseline/2",
11093
11623
  project: { name: truth.config.name, rootCommits: repo.value.rootCommitOids },
@@ -11118,7 +11648,8 @@ async function captureAdoptionBaseline(root, options) {
11118
11648
  findingsAt: "HEAD",
11119
11649
  findings: evaluated.findings,
11120
11650
  limitations: limitations2,
11121
- analysis
11651
+ analysis,
11652
+ evaluatedRules
11122
11653
  };
11123
11654
  const id3 = historyRecordId("adoption", body);
11124
11655
  const existing = loadAdoptionBaseline(root);
@@ -11290,180 +11821,6 @@ function oneGroup(groups, nodes) {
11290
11821
  return g !== void 0 && nodes.length > 1 && nodes.every((n) => groups.groups.get(n.id) === g);
11291
11822
  }
11292
11823
 
11293
- // packages/director/dist/context/policy.js
11294
- var CONTEXT_POLICY_VERSION = "2";
11295
- var EDGE_WEIGHTS = {
11296
- GOVERNS: 1,
11297
- IMPLEMENTS: 0.9,
11298
- TRACKED_BY: 0.9,
11299
- VALIDATED_BY: 0.8,
11300
- REQUIRES: 0.7,
11301
- CALLS: 0.7,
11302
- CONTAINS: 0.6,
11303
- IMPORTS: 0.5,
11304
- CHANGED_WITH: 0.3
11305
- };
11306
- var SEED_STRENGTH = { id: 1, path: 1, symbol: 1, "symbol-name": 0.9, keyword: 0.6, diff: 1 };
11307
- var CROSS_LANGUAGE_FACTOR = 0.5;
11308
- var DEFAULT_LIMITS = {
11309
- /** Upper bound; project.yaml context.max_depth (1–3, default 2) replaces it. */
11310
- maxDepth: 2,
11311
- /** Finalized candidates (05 nodeLimit 200). */
11312
- nodeLimit: 200,
11313
- /** Edges read per expanded node. */
11314
- edgeLimit: 2e3
11315
- };
11316
- var MIN_BUDGET = 1e3;
11317
- var MAX_BUDGET = 1e6;
11318
- var TASK_TOKEN_LIMIT = 200;
11319
- var KEYWORD = {
11320
- /** Keyword seeds kept (05: top 8). */
11321
- top: 8,
11322
- /** Keyword matches below this fraction of the best one are not seeds. */
11323
- minFraction: 0.3,
11324
- /** BM25 parameters. */
11325
- k1: 1.2,
11326
- b: 0.75
11327
- };
11328
- var SYMBOL_NAME_MAX_MATCHES = 5;
11329
- var TIER_ORDER = [
11330
- "requirement",
11331
- "decision",
11332
- "pending",
11333
- "code-direct",
11334
- "test",
11335
- "issue",
11336
- "code-structural",
11337
- "code-historical"
11338
- ];
11339
- var TIER_SHARE = {
11340
- requirement: 0.25,
11341
- decision: 0.15,
11342
- pending: 0.05,
11343
- "code-direct": 0.3,
11344
- test: 0.12,
11345
- issue: 0.05,
11346
- "code-structural": 0.06,
11347
- "code-historical": 0.02
11348
- };
11349
- var L3_PER_FILE_FIRST_PASS = 2;
11350
- var LEADING_CONTEXT_LINES = 12;
11351
- var GENERIC_FILE_WINDOW = { lines: 40, chars: 2e3 };
11352
-
11353
- // packages/director/dist/context/expand.js
11354
- var HUBS = /* @__PURE__ */ new Set(["project", "milestone", "decision", "issue"]);
11355
- function displayRef(id3) {
11356
- const ref = parseNodeId(id3);
11357
- if (ref === void 0)
11358
- return id3;
11359
- switch (ref.type) {
11360
- case "project":
11361
- return "project";
11362
- case "file":
11363
- return ref.path;
11364
- case "symbol":
11365
- return `${ref.path}#${ref.symbol}`;
11366
- case "test":
11367
- return `${ref.path}#${ref.name}`;
11368
- default:
11369
- return ref.id;
11370
- }
11371
- }
11372
- function better(a, b) {
11373
- return b.score - a.score || a.depth - b.depth || compareUtf8(a.node.id, b.node.id);
11374
- }
11375
- var Heap = class {
11376
- items = [];
11377
- get size() {
11378
- return this.items.length;
11379
- }
11380
- push(c) {
11381
- const a = this.items;
11382
- a.push(c);
11383
- let i = a.length - 1;
11384
- while (i > 0) {
11385
- const p = i - 1 >> 1;
11386
- if (better(a[i], a[p]) >= 0)
11387
- break;
11388
- [a[i], a[p]] = [a[p], a[i]];
11389
- i = p;
11390
- }
11391
- }
11392
- pop() {
11393
- const a = this.items;
11394
- const top = a[0];
11395
- const last = a.pop();
11396
- if (a.length > 0 && last !== void 0) {
11397
- a[0] = last;
11398
- let i = 0;
11399
- for (; ; ) {
11400
- const l = 2 * i + 1, r = l + 1;
11401
- let m = i;
11402
- if (l < a.length && better(a[l], a[m]) < 0)
11403
- m = l;
11404
- if (r < a.length && better(a[r], a[m]) < 0)
11405
- m = r;
11406
- if (m === i)
11407
- break;
11408
- [a[i], a[m]] = [a[m], a[i]];
11409
- i = m;
11410
- }
11411
- }
11412
- return top;
11413
- }
11414
- };
11415
- function step(edge) {
11416
- const provenance = typeof edge.metadata.provenance === "string" ? edge.metadata.provenance : "unknown";
11417
- return { from: displayRef(edge.from), type: edge.type, to: displayRef(edge.to), provenance };
11418
- }
11419
- function expandCandidates(store, seeds, options) {
11420
- const heap = new Heap();
11421
- const best = /* @__PURE__ */ new Map();
11422
- const seedIds = new Set(seeds.map((s) => s.id));
11423
- for (const s of seeds) {
11424
- const ref = parseNodeId(s.id);
11425
- const node = ref === void 0 ? void 0 : store.getNode(ref);
11426
- if (node === void 0)
11427
- continue;
11428
- heap.push({ node, score: s.strength, depth: 0, seed: s.id, steps: [] });
11429
- best.set(s.id, Math.max(best.get(s.id) ?? 0, s.strength));
11430
- }
11431
- const done = /* @__PURE__ */ new Map();
11432
- let truncated = false;
11433
- while (heap.size > 0) {
11434
- const c = heap.pop();
11435
- if (done.has(c.node.id))
11436
- continue;
11437
- if (done.size >= options.nodeLimit) {
11438
- truncated = true;
11439
- break;
11440
- }
11441
- done.set(c.node.id, c);
11442
- if (c.depth >= options.maxDepth || HUBS.has(c.node.type) && !(c.depth === 0 && seedIds.has(c.node.id)))
11443
- continue;
11444
- const adjacent = store.adjacentEdges([c.node.ref], { direction: "both", limit: options.edgeLimit });
11445
- if (adjacent.truncated)
11446
- truncated = true;
11447
- for (const edge of adjacent.edges) {
11448
- if (edge.type === "SUPERSEDES")
11449
- continue;
11450
- const other = edge.from === c.node.id ? edge.to : edge.from;
11451
- if (done.has(other) || other.startsWith("project:"))
11452
- continue;
11453
- const score = c.score * EDGE_WEIGHTS[edge.type];
11454
- if (score <= (best.get(other) ?? 0))
11455
- continue;
11456
- const ref = parseNodeId(other);
11457
- const node = ref === void 0 ? void 0 : store.getNode(ref);
11458
- if (node === void 0)
11459
- continue;
11460
- best.set(other, score);
11461
- heap.push({ node, score, depth: c.depth + 1, seed: c.seed, steps: [...c.steps, step(edge)] });
11462
- }
11463
- }
11464
- return { candidates: [...done.values()].sort(better), truncated };
11465
- }
11466
-
11467
11824
  // packages/director/dist/context/types.js
11468
11825
  var SEED_PROVENANCE = {
11469
11826
  id: "explicit",
@@ -11633,6 +11990,7 @@ function planContext(input) {
11633
11990
  const mil = new Map(truth.milestones.map((m) => [m.id, m]));
11634
11991
  const seedIds = new Set(input.seeds.seeds.map((s) => s.id));
11635
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));
11636
11994
  const slice = (loc2) => {
11637
11995
  const r = reader.slice(loc2);
11638
11996
  diagnostics.push(...r.diagnostics);
@@ -11665,6 +12023,7 @@ function planContext(input) {
11665
12023
  score: c.score,
11666
12024
  depth: c.depth,
11667
12025
  mandatory: exactSeeds.has(c.node.id),
12026
+ ...c.depth === 0 && idSeeds.has(c.node.id) ? { primary: true } : {},
11668
12027
  levels: levels([["L1", head], ["L2", body === "" ? void 0 : `${head}
11669
12028
  ${body}`], ["L3", exact === void 0 ? void 0 : `${head}
11670
12029
  ${fence(exact, "md")}`]]),
@@ -11773,7 +12132,7 @@ ${fence(exact, "yaml")}`]]),
11773
12132
  case "issue": {
11774
12133
  const i = iss.get(ref.id);
11775
12134
  if (i !== void 0)
11776
- put(issueDraft(i, node, c, slice, via(c)));
12135
+ put(issueDraft(i, node, c, slice, via(c), c.depth === 0 && idSeeds.has(node.id)));
11777
12136
  break;
11778
12137
  }
11779
12138
  case "milestone": {
@@ -11929,6 +12288,7 @@ ${fence(w.text, langOf(ref.path))}`;
11929
12288
  tier: d.tier,
11930
12289
  rank: i + 1,
11931
12290
  mandatory: d.mandatory,
12291
+ ...d.primary === true ? { primary: true } : {},
11932
12292
  levels: d.levels,
11933
12293
  via: d.via,
11934
12294
  ...d.source === void 0 ? {} : { source: d.source },
@@ -11965,7 +12325,7 @@ function proposalRefs(p) {
11965
12325
  out.add(e.id);
11966
12326
  return [...out].sort(compareUtf8);
11967
12327
  }
11968
- function issueDraft(i, node, c, slice, v) {
12328
+ function issueDraft(i, node, c, slice, v, primary) {
11969
12329
  const head = `${i.id} ${i.title} (${[i.status, i.milestone].filter((x) => x !== null).join(", ")})`;
11970
12330
  const ac = i.acceptance.map((a) => `- ${a.id} ${a.text}`);
11971
12331
  const commits = Array.isArray(node.payload.commits) ? node.payload.commits.slice(0, 3).map((x) => String(x).slice(0, 12)) : [];
@@ -11978,6 +12338,7 @@ function issueDraft(i, node, c, slice, v) {
11978
12338
  score: c.score,
11979
12339
  depth: c.depth,
11980
12340
  mandatory: c.depth === 0,
12341
+ ...primary ? { primary: true } : {},
11981
12342
  levels: levels([
11982
12343
  ["L1", head],
11983
12344
  ["L2", ac.length === 0 ? void 0 : [head, ...ac].join("\n")],
@@ -12237,8 +12598,14 @@ var STATIC_LIMITATIONS = [
12237
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." },
12238
12599
  { code: "no-diff", message: "The working-tree diff is not part of this packet." }
12239
12600
  ];
12240
- function limitations(plan, taskTruncated, omitted, summarized) {
12601
+ function limitations(plan, taskTruncated, omitted, summarized, budget, truncatedSeeds) {
12241
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
+ }
12242
12609
  if (omitted > 0)
12243
12610
  out.push({ code: "omitted", message: `omitted: ${omitted} lower-ranked candidates did not fit the budget (ids in omittedCandidates).` });
12244
12611
  if (summarized > 0)
@@ -12261,6 +12628,11 @@ function packContext(input) {
12261
12628
  const block = (text4) => meter.count(renderItemBlock(text4) + "\n");
12262
12629
  const evidence = (i) => meter.count(renderEvidenceLine(i.ref, i.via) + "\n");
12263
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
+ });
12264
12636
  const pendingTokens = (p, level) => block(p.levels[level]?.text ?? "");
12265
12637
  const build = () => {
12266
12638
  const chosen = items.filter((s) => s.level >= 0);
@@ -12298,6 +12670,7 @@ function packContext(input) {
12298
12670
  const omitted = items.filter((s) => s.level < 0);
12299
12671
  const summarized = chosen.filter((s) => s.level < s.value.levels.length - 1).length;
12300
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));
12301
12674
  const packet2 = {
12302
12675
  format: "duo.context-packet/1",
12303
12676
  request,
@@ -12309,7 +12682,7 @@ function packContext(input) {
12309
12682
  issues: of((i) => i.kind === "issue" || i.kind === "milestone"),
12310
12683
  pendingDecisions: pend,
12311
12684
  evidence: chosen.map((s) => ({ id: s.value.id, ref: s.value.ref, via: s.value.via })),
12312
- limitations: limitations(plan, taskTruncated, omitted.length, summarized),
12685
+ limitations: limitations(plan, taskTruncated, omitted.length, summarized, request.budget, truncatedSeeds),
12313
12686
  signals: plan.signals,
12314
12687
  omittedCandidates: omitted.map((s) => ({ id: s.value.id, ref: s.value.ref, rank: s.value.rank, tier: s.value.tier, reason: "budget" })),
12315
12688
  truncated: omitted.length > 0 || plan.traversalTruncated || taskTruncated,
@@ -12320,7 +12693,7 @@ function packContext(input) {
12320
12693
  return { packet: packet2, markdown: renderContextMarkdown(packet2) };
12321
12694
  };
12322
12695
  const frame = build();
12323
- 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)));
12324
12697
  const reserved = meter.count(renderContextMarkdown({ ...frame.packet, limitations: frameLimits }));
12325
12698
  if (reserved > request.budget) {
12326
12699
  return failure([createDiagnostic("CONTEXT_REQUEST_INVALID", `budget ${request.budget} is smaller than the packet frame (${reserved} tokens: task, headings, pending decisions, limitations)`)]);
@@ -12355,6 +12728,12 @@ function packContext(input) {
12355
12728
  for (const s of items)
12356
12729
  if (s.value.mandatory)
12357
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
+ }
12358
12737
  const promotion = available - spent;
12359
12738
  const byTier = (tier) => tier === "pending" ? pending : items.filter((s) => s.value.tier === tier);
12360
12739
  for (const capped of [true, false]) {
@@ -14466,88 +14845,6 @@ async function externalSourceDrift(ctx) {
14466
14845
  return { claims, limitations: limitations2 };
14467
14846
  }
14468
14847
 
14469
- // packages/director/dist/review/seeds.js
14470
- var PROPOSALS2 = `${STATE_DIR_NAME}/decisions/proposals/`;
14471
- function hunkRange(h) {
14472
- return h.newLines > 0 ? [h.newStart, h.newStart + h.newLines - 1] : [Math.max(1, h.newStart), Math.max(1, h.newStart)];
14473
- }
14474
- function overlaps(loc2, range) {
14475
- if (loc2?.startLine === void 0)
14476
- return false;
14477
- const end2 = loc2.endLine ?? loc2.startLine;
14478
- return range[0] <= end2 && range[1] >= loc2.startLine;
14479
- }
14480
- function owned(graph, path35) {
14481
- const out = [];
14482
- let afterId;
14483
- for (; ; ) {
14484
- const page = graph.listNodes({ ownerFile: path35, limit: 1e3, ...afterId === void 0 ? {} : { afterId } });
14485
- out.push(...page);
14486
- if (page.length < 1e3)
14487
- return out;
14488
- afterId = page[page.length - 1]?.id;
14489
- }
14490
- }
14491
- var width = (loc2) => (loc2?.endLine ?? 0) - (loc2?.startLine ?? 0);
14492
- function truthDefinitions(truth) {
14493
- return [
14494
- ...truth.requirements.map((d) => ({ ref: definitionRef("requirement", d.id), location: d.location })),
14495
- ...truth.decisions.map((d) => ({ ref: definitionRef("decision", d.id), location: d.location })),
14496
- ...truth.constraints.map((d) => ({ ref: definitionRef("decision", d.id), location: d.location })),
14497
- ...truth.issues.map((d) => ({ ref: definitionRef("issue", d.id), location: d.location })),
14498
- ...truth.milestones.map((d) => ({ ref: definitionRef("milestone", d.id), location: d.location }))
14499
- ];
14500
- }
14501
- function diffSeeds(files, graph, truth) {
14502
- const seeds = /* @__PURE__ */ new Map();
14503
- const put = (entity, reason, path35, evidenceId2) => {
14504
- if (graph.getNode(entity) === void 0)
14505
- return;
14506
- const id3 = nodeId(entity);
14507
- const s = seeds.get(id3) ?? { entity, reason, path: path35, evidence: /* @__PURE__ */ new Set() };
14508
- s.evidence.add(evidenceId2);
14509
- seeds.set(id3, s);
14510
- };
14511
- const defs = truthDefinitions(truth);
14512
- for (const f of files) {
14513
- if (f.kind === "deleted")
14514
- continue;
14515
- if (f.path.startsWith(`${STATE_DIR_NAME}/`)) {
14516
- if (f.path.startsWith(PROPOSALS2))
14517
- continue;
14518
- for (const h of f.hunks) {
14519
- for (const d of defs)
14520
- if (d.location.path === f.path && overlaps(d.location, hunkRange(h)))
14521
- put(d.ref, "truth-changed", f.path, h.evidenceId);
14522
- }
14523
- continue;
14524
- }
14525
- const nodes = owned(graph, f.path).filter((n) => n.type === "symbol" || n.type === "test");
14526
- const ranges = new Map(nodes.map((n) => [n.id, nodeLocations(n)]));
14527
- let touched = false;
14528
- for (const h of f.hunks) {
14529
- const range = hunkRange(h);
14530
- const hits = nodes.map((n) => ({ n, at: (ranges.get(n.id) ?? []).filter((l) => overlaps(l, range)) })).filter((x) => x.at.length > 0);
14531
- 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])))));
14532
- for (const { n } of inner) {
14533
- put(n.ref, "hunk-overlap", f.path, h.evidenceId);
14534
- touched = true;
14535
- }
14536
- }
14537
- if (!touched)
14538
- for (const e of f.evidenceIds)
14539
- put(fileRef(f.path), "file-changed", f.path, e);
14540
- }
14541
- return [...seeds.entries()].sort(([a], [b]) => compareUtf8(a, b)).map(([id3, s]) => ({
14542
- id: id3,
14543
- ref: displayRef(id3),
14544
- entity: s.entity,
14545
- reason: s.reason,
14546
- path: s.path,
14547
- evidenceIds: [...s.evidence].sort(compareUtf8)
14548
- }));
14549
- }
14550
-
14551
14848
  // packages/director/dist/review/semantic.js
14552
14849
  var ALIGNMENTS = ["ALIGNED", "PARTIAL", "CONFLICT", "UNKNOWN"];
14553
14850
  function semanticSchema(claimIds) {
@@ -14853,17 +15150,21 @@ async function reviewChanges(root, request, options) {
14853
15150
  stateDiagnostics: readIndexState(root).state?.diagnostics ?? [],
14854
15151
  ...request.testResults === void 0 ? {} : { testResults: request.testResults },
14855
15152
  ...adoption.baseline === void 0 ? {} : { baselineKeys: new Set(adoption.baseline.findings.map((f) => f.key)) },
14856
- ...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 }
14857
15156
  };
14858
15157
  const integrity = await decisionIntegrity(ctx);
14859
- const forbids = await decisionForbids(ctx);
15158
+ const forbids2 = await decisionForbids(ctx);
15159
+ const imports = await decisionForbidsImport(ctx);
14860
15160
  const drift = await externalSourceDrift(ctx);
14861
15161
  const additions = unlinkedAdditions(ctx);
14862
- const conflicted = new Set(forbids.map((c) => c.subject.id));
15162
+ const conflicted = new Set([...forbids2, ...imports.claims].map((c) => c.subject.id));
14863
15163
  const claims = [
14864
15164
  ...integrity,
14865
15165
  ...supersedeIntegrity(ctx),
14866
- ...forbids,
15166
+ ...forbids2,
15167
+ ...imports.claims,
14867
15168
  ...decisionGovernance(ctx, conflicted),
14868
15169
  ...declaredReferences(ctx),
14869
15170
  ...requirementImplementation(ctx),
@@ -14914,6 +15215,7 @@ async function reviewChanges(root, request, options) {
14914
15215
  ...limitationsOf({ identity: diff.value.identity, from: diff.value.from, to: diff.value.to, files }, reviewPacket, task !== "", taskContext?.status === "ready"),
14915
15216
  ...analysisLimitsOf(files, options.graph),
14916
15217
  ...drift.limitations,
15218
+ ...imports.limitations,
14917
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." }] : []
14918
15220
  ],
14919
15221
  semanticAssist: semantic.assist,
@@ -14934,6 +15236,25 @@ async function reviewChanges(root, request, options) {
14934
15236
  return success({ result, performance: perf() }, result.diagnostics);
14935
15237
  }
14936
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
+
14937
15258
  // packages/director/dist/runtime/metrics.js
14938
15259
  import fs23 from "node:fs";
14939
15260
  import fsp5 from "node:fs/promises";
@@ -15918,6 +16239,16 @@ var repoPattern = z4.string().min(1).max(4096).refine((p) => normalizeRepoPatter
15918
16239
  var definitionId = z4.string().regex(DEFINITION_ID_PATTERN, "a definition ID such as AUTH-03 or D-004");
15919
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");
15920
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");
15921
16252
  var INPUT = {
15922
16253
  duo_get_status: z4.strictObject({}),
15923
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() }),
@@ -15940,6 +16271,10 @@ var INPUT = {
15940
16271
  answer: text2(2e3),
15941
16272
  rationale: text2(4e3).optional(),
15942
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(),
15943
16278
  agent: text2(100).optional()
15944
16279
  })
15945
16280
  };
@@ -16058,7 +16393,7 @@ ${advice.join("\n")}`);
16058
16393
  const r = p;
16059
16394
  if (r.status === "index-required")
16060
16395
  return "INDEX_REQUIRED: the DUO index is not current. Run duoctl index, then review again.";
16061
- 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]" : ""}`);
16062
16397
  const a = r.semanticAssist;
16063
16398
  const semantic = a.status === "not-requested" ? [] : [
16064
16399
  `Semantic assistance (supplemental, never blocks): ${a.status}${a.failure === void 0 ? "" : ` (${a.failure})`}`,
@@ -16111,14 +16446,17 @@ ${advice.join("\n")}`);
16111
16446
  name: "duo_propose_decision",
16112
16447
  title: "Propose a Decision",
16113
16448
  readOnly: false,
16114
- 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.`,
16115
16450
  run: async (args, ctx) => {
16116
16451
  const op = await proposeDecision(ctx.root, args.agent ?? ctx.agentName, {
16117
16452
  title: args.title,
16118
16453
  question: args.question,
16119
16454
  answer: args.answer,
16120
16455
  ...args.rationale === void 0 ? {} : { rationale: args.rationale },
16121
- ...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 }
16122
16460
  });
16123
16461
  return { op, metric: { status: op.kind === "ok" ? "proposed" : op.kind } };
16124
16462
  },
@@ -17304,8 +17642,8 @@ function projectProposals(root) {
17304
17642
  const service = createDecisionService({ root });
17305
17643
  const previews = {};
17306
17644
  for (const e of d.proposals.filter((x) => x.status === "pending")) {
17307
- const preview = await service.previewConfirm(e.id);
17308
- 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) };
17309
17647
  }
17310
17648
  return { kind: "ok", diagnostics: [], payload: { proposals: d.proposals, previews } };
17311
17649
  });
@@ -17462,7 +17800,8 @@ var PROPOSAL_ID = /^P-[A-Za-z0-9-]{1,64}$/u;
17462
17800
  var SCHEMAS = {
17463
17801
  context: z5.strictObject({ task: text3(2e4), budget: z5.number().int().min(MIN_BUDGET).max(MAX_BUDGET).optional() }),
17464
17802
  review: z5.strictObject({ task: text3(2e4).optional(), from: endpoint2.optional(), to: endpoint2.optional(), includeSemanticAssist: z5.boolean().optional() }),
17465
- 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) }),
17466
17805
  reject: z5.strictObject({ reason: z5.string().max(2e3).optional() }),
17467
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) }),
17468
17807
  search: z5.strictObject({ q: text3(200), limit: z5.coerce.number().int().min(1).max(50).default(20) }),
@@ -17519,7 +17858,7 @@ async function startDuoUiServer(options) {
17519
17858
  const body2 = parse3(SCHEMAS.confirm, await readJson2(req));
17520
17859
  if (body2.confirmId.trim() !== id3)
17521
17860
  throw new HttpError(400, "UI_CONFIRM_ID_MISMATCH", "type the proposal ID to confirm it");
17522
- const r2 = await service.confirm(await actor(), id3);
17861
+ const r2 = await service.confirm(await actor(), id3, { expectedDigest: body2.previewDigest });
17523
17862
  await meter("decision", r2.value === void 0 ? "failed" : "confirmed", started);
17524
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 })) } };
17525
17864
  }
@@ -17802,6 +18141,22 @@ var EN = {
17802
18141
  "decision.confirmed": "confirmed as {id} \xB7 {path}",
17803
18142
  "decision.rejected": "rejected {id}",
17804
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.",
17805
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:",
17806
18161
  "stats.none": "No metrics yet.",
17807
18162
  "not-initialized": "Not a DUO project (no .duo-project/project.yaml). Run duoctl init.",
@@ -17996,6 +18351,22 @@ var KO = {
17996
18351
  "decision.confirmed": "{id}\uB85C \uD655\uC815 \xB7 {path}",
17997
18352
  "decision.rejected": "{id} \uAC70\uC808",
17998
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.",
17999
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:",
18000
18371
  "stats.none": "\uC9C0\uD45C\uAC00 \uC544\uC9C1 \uC5C6\uC2B5\uB2C8\uB2E4.",
18001
18372
  "not-initialized": "DUO project\uAC00 \uC544\uB2D9\uB2C8\uB2E4(.duo-project/project.yaml \uC5C6\uC74C). duoctl init\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
@@ -18258,6 +18629,59 @@ function ambiguityHint(L, a) {
18258
18629
  }
18259
18630
 
18260
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
+ }
18261
18685
  async function decisionCommand(env, sub, id3, reason) {
18262
18686
  const project2 = requireProject(env, "decision");
18263
18687
  if (project2.value === void 0)
@@ -18281,27 +18705,31 @@ async function decisionCommand(env, sub, id3, reason) {
18281
18705
  if (!env.io.isTTY || env.nonInteractive) {
18282
18706
  return failed4("decision", EXIT.ERROR, [createDiagnostic("CLI_TTY_REQUIRED", t("en", "decision.tty"))], [t(env.locale, "decision.tty")]);
18283
18707
  }
18284
- const proposal = entries.find((e) => e.id === id3);
18285
- const decision = truth.decisions.find((d) => d.id === id3);
18286
- const shown = proposal?.proposal ?? decision;
18287
- if (shown !== void 0) {
18288
- env.io.err(`${id3} "${shown.title}"`);
18289
- env.io.err(` question ${shown.question}`);
18290
- env.io.err(` answer ${shown.answer}`);
18291
- if (shown.governs.requirements.length > 0)
18292
- 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);
18293
18715
  }
18294
18716
  const typed = (await env.io.prompt(t(env.locale, "decision.retype", { op: sub })))?.trim();
18295
18717
  if (typed !== id3)
18296
18718
  return failed4("decision", EXIT.ERROR, [], [t(env.locale, "decision.mismatch")]);
18297
- const service = createDecisionService({ root: env.root, clock: () => env.io.now() });
18298
18719
  const actor = await humanActor(env.root);
18299
18720
  if (sub === "confirm") {
18300
- const r2 = await service.confirm(actor, id3);
18721
+ const r2 = await service.confirm(actor, id3, previewed?.value === void 0 ? {} : { expectedDigest: previewed.value.digest });
18301
18722
  if (r2.value === void 0)
18302
18723
  return failed4("decision", EXIT.ERROR, r2.diagnostics, diagLines(r2.diagnostics), null, { status: "failed" });
18303
18724
  const warnings = r2.diagnostics.filter((d) => d.severity === "warning").map((d) => `${d.code}: ${d.message}`);
18304
- 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
+ };
18305
18733
  }
18306
18734
  const r = await service.reject(actor, id3, reason);
18307
18735
  if (r.value === void 0)
@@ -18823,7 +19251,7 @@ function renderReview(env, r) {
18823
19251
  const ev = new Map(r.evidence.map((e) => [e.id, e]));
18824
19252
  const shown = r.claims.filter((c) => env.verbose || c.alignment !== "ALIGNED");
18825
19253
  for (const c of shown) {
18826
- 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(", ");
18827
19255
  out.push(` ${c.alignment.padEnd(9)} ${c.rule.padEnd(26)} ${c.subject.id} \xB7 ${c.reason}${tags === "" ? "" : ` [${tags}]`}`);
18828
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]}`}`);
18829
19257
  if (pointers.length > 0)