@c4a/context-cli 0.5.29-beta.16 → 0.5.29-beta.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -19673,15 +19673,40 @@ function isEvidenceEchoDetail(detail, basisTexts = []) {
19673
19673
  return true;
19674
19674
  return stripped.label !== undefined && PREFIX_ONLY_ECHO_LABEL_RE.test(stripped.label);
19675
19675
  }
19676
+ function fencedBlocksIn(value) {
19677
+ return [...value.matchAll(FENCED_DETAIL_BLOCK_RE)].map((match) => normalizeMarkdown(match[0]).trim()).filter((block) => block.length > 0);
19678
+ }
19679
+ function basisText(value) {
19680
+ return typeof value === "string" ? value : value.join(`
19681
+ `);
19682
+ }
19683
+ function hasCitedFencedDetail(detail, basisTexts) {
19684
+ const cited = normalizeMarkdown(basisText(basisTexts)).trim();
19685
+ if (cited.length === 0)
19686
+ return false;
19687
+ return fencedBlocksIn(detail).some((block) => cited.includes(block));
19688
+ }
19689
+ function isOnlyCitedFencedDetail(detail, basisTexts) {
19690
+ const normalizedDetail = normalizeMarkdown(detail).trim();
19691
+ if (normalizedDetail.length === 0)
19692
+ return false;
19693
+ const cited = normalizeMarkdown(basisText(basisTexts)).trim();
19694
+ const blocks = fencedBlocksIn(normalizedDetail);
19695
+ if (cited.length === 0 || blocks.length === 0 || !blocks.every((block) => cited.includes(block)))
19696
+ return false;
19697
+ return normalizedDetail.replace(FENCED_DETAIL_BLOCK_RE, "").trim().length === 0;
19698
+ }
19676
19699
  function renderableSectionDetail(detail) {
19677
19700
  if (detail === undefined || detail.trim().length === 0)
19678
19701
  return;
19679
19702
  return isEvidenceEchoDetail(detail) ? undefined : detail;
19680
19703
  }
19681
- var EVIDENCE_ECHO_PREFIX_RE, PREFIX_ONLY_ECHO_LABEL_RE;
19704
+ var EVIDENCE_ECHO_PREFIX_RE, PREFIX_ONLY_ECHO_LABEL_RE, FENCED_DETAIL_BLOCK_RE;
19682
19705
  var init_sectionDetail = __esm(() => {
19706
+ init_normalize();
19683
19707
  EVIDENCE_ECHO_PREFIX_RE = /^\s*(原文|原始文本|原始引用|引用|证据|source|raw|quote|original)\s*[::]\s*/iu;
19684
19708
  PREFIX_ONLY_ECHO_LABEL_RE = /^(?:原文|原始文本|原始引用|引用|证据)$/iu;
19709
+ FENCED_DETAIL_BLOCK_RE = /```[^\n`]*\n[\s\S]*?```|~~~[^\n~]*\n[\s\S]*?~~~/gu;
19685
19710
  });
19686
19711
 
19687
19712
  // src/lib/nodeRenderer.ts
@@ -28706,13 +28731,6 @@ function textIsUrlReferenceOnly(content, citedText) {
28706
28731
  const requiredMatches = Math.max(1, Math.ceil(lexical.contentTermCount * 0.5));
28707
28732
  return lexical.matchedTermCount >= requiredMatches && lexical.missingTerms.length <= 2;
28708
28733
  }
28709
- function fencedBlocksIn(text) {
28710
- return [...text.matchAll(FENCED_CODE_RE2)].map((match) => normalizeMarkdown(match[0]).trim()).filter((block) => block.length > 0);
28711
- }
28712
- function hasCitedFencedDetail(detail, citedText) {
28713
- const cited = normalizeMarkdown(citedText).trim();
28714
- return fencedBlocksIn(detail).some((block) => cited.includes(block));
28715
- }
28716
28734
  function urlSupportFor(content, citedText) {
28717
28735
  const contentUrls = evidenceUrlsIn(content);
28718
28736
  const citedUrls = new Set(evidenceUrlsIn(citedText));
@@ -28806,11 +28824,13 @@ function supportLexicalOverlap(content, citedText) {
28806
28824
  function hardTermPresent(term, citedText, citedHardTerms) {
28807
28825
  if (citedHardTerms.has(term) || normalizedToken(citedText).includes(term))
28808
28826
  return true;
28809
- const slashParts = term.split("/").map(normalizedToken).filter((part) => part.length > 0);
28810
- if (slashParts.length <= 1)
28811
- return false;
28812
28827
  const normalizedCited = normalizedToken(citedText);
28813
- return slashParts.every((part) => citedHardTerms.has(part) || normalizedCited.includes(part));
28828
+ const slashParts = term.split("/").map(normalizedToken).filter((part) => part.length > 0);
28829
+ if (slashParts.length > 1) {
28830
+ return slashParts.every((part) => citedHardTerms.has(part) || normalizedCited.includes(part));
28831
+ }
28832
+ const hyphenParts = term.includes("-") && !/[0-9._/:#]/u.test(term) ? term.split("-").map(normalizedToken).filter((part) => part.length > 1) : [];
28833
+ return hyphenParts.length > 1 && hyphenParts.every((part) => citedHardTerms.has(part) || normalizedCited.includes(part));
28814
28834
  }
28815
28835
  function thresholdsForKind(kind) {
28816
28836
  return kind === "spec" || kind === "comparison" || kind === "decision" ? { supported: 0.5, weak: 0.3 } : DEFAULT_THRESHOLDS;
@@ -28872,8 +28892,7 @@ function sourceTextSupportDiagnostic(content, citedText, thresholds = DEFAULT_TH
28872
28892
  const requiredMatches = Math.max(1, Math.ceil(contentTermCount * thresholds.supported));
28873
28893
  const overlapRatio = matchedTermCount / contentTermCount;
28874
28894
  const shortText = contentTermCount <= 10;
28875
- const hasHardFacts = contentHardTerms.length > 0;
28876
- const baseVerdict = missingHardTerms.length > 0 ? "unsupported" : overlapRatio >= thresholds.supported || shortText && lexical.missingTerms.length <= 2 ? "supported" : overlapRatio >= thresholds.weak || shortText && lexical.missingTerms.length <= 4 ? "weak" : hasHardFacts && matchedTermCount > 0 ? "weak" : "unsupported";
28895
+ const baseVerdict = missingHardTerms.length > 0 ? "unsupported" : overlapRatio >= thresholds.supported || shortText && lexical.missingTerms.length <= 2 ? "supported" : overlapRatio >= thresholds.weak || shortText && lexical.missingTerms.length <= 4 ? "weak" : matchedTermCount > 0 ? "weak" : "unsupported";
28877
28896
  const urlAwareVerdict = urlSupport.contentUrls.length > 0 && urlSupport.missingUrls.length === 0 && missingHardTerms.length === 0 ? urlSupport.referenceOnly && (baseVerdict === "weak" || baseVerdict === "unsupported") ? "supported" : baseVerdict === "unsupported" ? "weak" : baseVerdict : baseVerdict;
28878
28897
  const verdict = missingPlaceholderTerms.length > 0 && urlAwareVerdict === "supported" ? "weak" : urlAwareVerdict;
28879
28898
  return {
@@ -29067,13 +29086,14 @@ function weakSourceSupportGuidance(action, verdict) {
29067
29086
  if (verdict !== "weak")
29068
29087
  return "";
29069
29088
  if (action === "keep_separate") {
29070
- return " Key facts match, but support is weak; ask the user to confirm the summary/compression during review, then apply the final keep_separate decision marked decided_by: user.";
29089
+ return " Key facts match, but support is weak; ask the user to confirm the summary/compression during review, then apply the final keep_separate decision marked decided_by: user. Auto mode or general permission to continue is not confirmation.";
29071
29090
  }
29072
29091
  return " Key facts match, but this action rewrites or reanchors existing knowledge and requires direct support; choose source_ref/source_refs from the prepared evidence, split the claim, or ask the user before choosing a different final action.";
29073
29092
  }
29074
- var DEFAULT_THRESHOLDS, CJK_ONLY_RE, CJK_CHAR_RE, URL_RE2, FENCED_CODE_RE2, TRAILING_URL_PUNCTUATION_RE, TRACKING_QUERY_PARAM_RE, EN_URL_REFERENCE_LABEL_RE, CJK_URL_REFERENCE_LABEL_RE, CONSTRAINT_PATTERNS, PLACEHOLDER_ANCHOR_PARTS;
29093
+ var DEFAULT_THRESHOLDS, CJK_ONLY_RE, CJK_CHAR_RE, URL_RE2, TRAILING_URL_PUNCTUATION_RE, TRACKING_QUERY_PARAM_RE, EN_URL_REFERENCE_LABEL_RE, CJK_URL_REFERENCE_LABEL_RE, CONSTRAINT_PATTERNS, PLACEHOLDER_ANCHOR_PARTS;
29075
29094
  var init_sourceSupport = __esm(() => {
29076
29095
  init_normalize();
29096
+ init_sectionDetail();
29077
29097
  init_sourceRef();
29078
29098
  init_ref();
29079
29099
  init_rawBlocks();
@@ -29082,7 +29102,6 @@ var init_sourceSupport = __esm(() => {
29082
29102
  CJK_ONLY_RE = /^[\u4e00-\u9fff]+$/u;
29083
29103
  CJK_CHAR_RE = /[\u4e00-\u9fff]/gu;
29084
29104
  URL_RE2 = /https?:\/\/[^\s)\]}>"')】》」』,。;:!?]+/giu;
29085
- FENCED_CODE_RE2 = /```[^\n`]*\n[\s\S]*?```/gu;
29086
29105
  TRAILING_URL_PUNCTUATION_RE = /[.,;:!?)\]\}>"',。;:!?)】》」』]+$/u;
29087
29106
  TRACKING_QUERY_PARAM_RE = /^(?:utm_|fbclid$|gclid$|mc_cid$|mc_eid$)/iu;
29088
29107
  EN_URL_REFERENCE_LABEL_RE = /\b(?:url|urls|link|links|doc|docs|document|documents|documentation|reference|references|page|pages|guide|guides|entry|entries|website|official|source|sources)\b/giu;
@@ -30012,6 +30031,33 @@ function workflowPayloadReceipt(record, extraOrOptions = []) {
30012
30031
  next_command: options.nextCommand ?? showCommand
30013
30032
  };
30014
30033
  }
30034
+ function stalePayloadAgentHints(input) {
30035
+ if (input.payload === "align-coarse-read") {
30036
+ const ledgerDigest = input.workflowInputDigests?.["align-candidate-ledger"];
30037
+ const ledgerCommand = ledgerDigest === undefined ? `context workflow show --payload align-candidate-ledger --scope ${shellQuote(input.resolvedScopeId)} --view ledger --unwrap --format json` : [
30038
+ "context workflow show --payload align-candidate-ledger",
30039
+ `--scope ${shellQuote(input.resolvedScopeId)}`,
30040
+ `--digest ${shellQuote(ledgerDigest)}`,
30041
+ "--view ledger",
30042
+ "--unwrap",
30043
+ "--format json"
30044
+ ].join(" ");
30045
+ return [{
30046
+ code: "workflow-input-digest-stale",
30047
+ severity: "error",
30048
+ message: "align-coarse-read is a latest checkpoint; earlier per-source reading notes are stored in align-candidate-ledger.source_readings.",
30049
+ next_action: "Read the align-candidate-ledger ledger view, optionally adding --source <source_id> to inspect a specific source reading.",
30050
+ command: ledgerCommand
30051
+ }];
30052
+ }
30053
+ return [{
30054
+ code: "workflow-input-digest-stale",
30055
+ severity: "error",
30056
+ message: "The submitted payload digest does not match the current workflow payload.",
30057
+ next_action: "Run `context workflow show --payload <name>` again and retry with the returned digest.",
30058
+ command: "context workflow show --payload <name>"
30059
+ }];
30060
+ }
30015
30061
  async function writeWorkflowPayloadWithDigest(input) {
30016
30062
  const path8 = workflowPayloadPath(input);
30017
30063
  const body2 = encodePayload(input.value, input.format);
@@ -30059,13 +30105,7 @@ async function readWorkflowPayloadByDigest(input) {
30059
30105
  workflow_id: input.workflowId,
30060
30106
  scope_id: scopeId,
30061
30107
  payload: input.payload,
30062
- agent_hints: [{
30063
- code: "workflow-input-digest-stale",
30064
- severity: "error",
30065
- message: "The submitted payload digest does not match the current workflow payload.",
30066
- next_action: "Run `context workflow show --payload <name>` again and retry with the returned digest.",
30067
- command: "context workflow show --payload <name>"
30068
- }]
30108
+ agent_hints: stalePayloadAgentHints({ ...input, resolvedScopeId: scopeId })
30069
30109
  });
30070
30110
  }
30071
30111
  return {
@@ -32047,8 +32087,11 @@ async function assertNoUnresolvedHighSignalCoverage(ctxDir) {
32047
32087
  code: "coverage-high-signal-unresolved",
32048
32088
  severity: "error",
32049
32089
  message: `${unresolved.length} high-signal coverage candidate(s) must be disposed before close.`,
32050
- next_action: "Read the node-scoped coverage-candidates payload, submit a coverage disposition patch, then rerun close.",
32051
- command: `context workflow show --payload coverage-candidates --scope ${nodeRunIdForSlug(unresolved[0].node_slug)} --view coverage`
32090
+ next_action: "Read the node-scoped coverage-candidates payload. If the remaining candidates are intentionally excluded for this node, run --coverage-skip-unresolved with an audit reason; otherwise submit a targeted disposition patch.",
32091
+ command: `context workflow show --payload coverage-candidates --scope ${nodeRunIdForSlug(unresolved[0].node_slug)} --view coverage`,
32092
+ diagnostics: {
32093
+ bulk_skip_command: `context compile --coverage-skip-unresolved --coverage-disposition-node ${unresolved[0].node_slug} --payload-digest <digest> --reason "<reason>"`
32094
+ }
32052
32095
  }]
32053
32096
  });
32054
32097
  }
@@ -35202,7 +35245,7 @@ var init_agentHintRegistry = __esm(() => {
35202
35245
  "verify-section-issues-run-compile",
35203
35246
  "verify-source-coverage-review",
35204
35247
  "verify-structural-issues-run-align",
35205
- "weak-source-support-needs-confirmation",
35248
+ "weak-source-support-warning",
35206
35249
  "workflow-abandon-id-mismatch",
35207
35250
  "workflow-active-node-run-rejected",
35208
35251
  "workflow-aggregate-summary",
@@ -35220,6 +35263,7 @@ var init_agentHintRegistry = __esm(() => {
35220
35263
  "workflow-input-digest-stale",
35221
35264
  "workflow-input-digests-empty",
35222
35265
  "workflow-ledger-summary",
35266
+ "workflow-ledger-source-filter",
35223
35267
  "workflow-ownership-by-node-summary",
35224
35268
  "workflow-payload-scope-ambiguous",
35225
35269
  "workflow-payload-view-unsupported",
@@ -35271,7 +35315,7 @@ var init_agentHintRegistry = __esm(() => {
35271
35315
  severity: "warning",
35272
35316
  description: "High-signal coverage candidates remain unresolved before close.",
35273
35317
  trigger: "A compile context/apply result sees unresolved high-signal coverage candidates for the current node.",
35274
- command: "context workflow show --payload coverage-candidates"
35318
+ command: 'context compile --coverage-skip-unresolved --coverage-disposition-node <slug> --payload-digest <digest> --reason "<reason>"'
35275
35319
  },
35276
35320
  {
35277
35321
  code: "review-safe-defaults-partial",
@@ -35441,8 +35485,8 @@ function excerptAround(value, index, radius = 80) {
35441
35485
  const suffix = end < value.length ? "..." : "";
35442
35486
  return `${prefix}${value.slice(start2, end)}${suffix}`;
35443
35487
  }
35444
- function normalizedDivergence(basisText, draftText) {
35445
- const basis = normalizeEvidenceText(basisText);
35488
+ function normalizedDivergence(basisText2, draftText) {
35489
+ const basis = normalizeEvidenceText(basisText2);
35446
35490
  const draft = normalizeEvidenceText(draftText);
35447
35491
  const max = Math.min(basis.length, draft.length);
35448
35492
  let offset = 0;
@@ -35462,8 +35506,8 @@ function normalizedExtractSegments(value) {
35462
35506
  function normalizeSegmentSearchText(value) {
35463
35507
  return normalizeEvidenceText(value).replace(/[.,;:!?,。;:!?]+/gu, " ").replace(/\s+/gu, " ").trim();
35464
35508
  }
35465
- function extractModeHint(field, basisText, draftText) {
35466
- const basis = normalizeSegmentSearchText(basisText);
35509
+ function extractModeHint(field, basisText2, draftText) {
35510
+ const basis = normalizeSegmentSearchText(basisText2);
35467
35511
  const draftSegments = normalizedExtractSegments(draftText);
35468
35512
  const segments = draftSegments.map((segment, index) => ({
35469
35513
  detail_order: index,
@@ -35714,7 +35758,7 @@ async function resolveBasisSpans(ctxDir, spans, slug, context, label) {
35714
35758
  function assertExtractContent(action, slug, context, label, resolvedBasisSpans, state) {
35715
35759
  if (action.content_mode !== "extract")
35716
35760
  return;
35717
- const basisText = resolvedBasisSpans.map((span) => span.text).join(`
35761
+ const basisText2 = resolvedBasisSpans.map((span) => span.text).join(`
35718
35762
  `);
35719
35763
  for (const [field, value] of [
35720
35764
  ["content", action.content],
@@ -35722,16 +35766,16 @@ function assertExtractContent(action, slug, context, label, resolvedBasisSpans,
35722
35766
  ]) {
35723
35767
  if (typeof value !== "string" || value.trim().length === 0)
35724
35768
  continue;
35725
- const normalizedMatch = normalizedIncludes(basisText, value);
35726
- const exactMatch = basisText.includes(value);
35769
+ const normalizedMatch = normalizedIncludes(basisText2, value);
35770
+ const exactMatch = basisText2.includes(value);
35727
35771
  if (!normalizedMatch) {
35728
- const modeHint = extractModeHint(field, basisText, value);
35772
+ const modeHint = extractModeHint(field, basisText2, value);
35729
35773
  throw draftError(slug, context, `extract ${field} must be a normalized substring of the cited source text`, {
35730
35774
  path: `${label}.${field}`,
35731
35775
  reasonCode: `extract-${field}-not-in-basis`,
35732
35776
  diffHint: modeHint !== null ? String(modeHint.next_action) : `${field} must be copied from the cited source text as one contiguous normalized substring when content_mode=extract. Use the diagnostics.divergence excerpts to fix the first mismatch, or switch to minimal_paraphrase when you intentionally rewrite or reorder text.`,
35733
35777
  diagnostics: {
35734
- divergence: normalizedDivergence(basisText, value),
35778
+ divergence: normalizedDivergence(basisText2, value),
35735
35779
  ...modeHint !== null ? { extract_mode_hint: modeHint } : {}
35736
35780
  }
35737
35781
  });
@@ -35753,6 +35797,9 @@ function assertNonEchoDetail(action, slug, context, label, resolvedBasisSpans) {
35753
35797
  const basisTexts = resolvedBasisSpans.map((span) => span.text);
35754
35798
  if (!isEvidenceEchoDetail(action.detail, basisTexts))
35755
35799
  return;
35800
+ const sectionKind = "kind" in action ? action.kind : context.existing?.sections.find((section) => section.id === action.target_section_id)?.kind;
35801
+ if (sectionKind === "example" && isOnlyCitedFencedDetail(action.detail, basisTexts))
35802
+ return;
35756
35803
  throw draftError(slug, context, `${label}.detail must not echo raw cited evidence`, {
35757
35804
  path: `${label}.detail`,
35758
35805
  reasonCode: "detail-evidence-echo",
@@ -36163,30 +36210,141 @@ async function archiveOutputArtifacts(options) {
36163
36210
  }
36164
36211
  var init_outputArchive = () => {};
36165
36212
 
36166
- // src/workflow/compileClose.ts
36213
+ // src/workflow/compileCloseFinalizedNodes.ts
36167
36214
  import { existsSync as existsSync43 } from "node:fs";
36168
- import { readFile as readFile51, readdir as readdir19, rm as rm13 } from "node:fs/promises";
36169
36215
  import { join as join52 } from "node:path";
36216
+ function finalizedContainerChildren(nodes, parent) {
36217
+ if (parent.type !== "domain")
36218
+ return [];
36219
+ return nodes.filter((node2) => node2.contains_parent === parent.slug).sort((left, right) => left.slug.localeCompare(right.slug));
36220
+ }
36221
+ function finalizedNodePath(ctxDir, node2) {
36222
+ return join52(ctxDir, "knowledge", node2.type, `${node2.slug}.md`);
36223
+ }
36224
+ function isExplicitNoWriteNode(node2) {
36225
+ return node2.planned_sections !== undefined && node2.planned_sections.length === 0;
36226
+ }
36227
+ function hasFinalizedGraphLink(ownership, node2) {
36228
+ if (typeof node2.contains_parent === "string" && node2.contains_parent.length > 0)
36229
+ return true;
36230
+ if ((ownership.nodes ?? []).some((candidate) => candidate.contains_parent === node2.slug))
36231
+ return true;
36232
+ return (ownership.edges ?? []).some((edge2) => edge2.from === node2.slug || edge2.to === node2.slug);
36233
+ }
36234
+ function hasNoWritePlaceholderSupport(ownership, node2) {
36235
+ return (node2.context_sources?.length ?? 0) > 0 || hasFinalizedGraphLink(ownership, node2);
36236
+ }
36237
+ async function ensureFinalizedContainerDomains(ctxDir, now) {
36238
+ const ownership = await readCurrentSourceOwnership(ctxDir);
36239
+ const nodes = ownership?.nodes ?? [];
36240
+ let created = 0;
36241
+ for (const node2 of nodes) {
36242
+ const children = finalizedContainerChildren(nodes, node2);
36243
+ if (children.length === 0)
36244
+ continue;
36245
+ if (existsSync43(finalizedNodePath(ctxDir, node2)))
36246
+ continue;
36247
+ await mdriveNodeCreate({
36248
+ ctxDir,
36249
+ input: {
36250
+ node: {
36251
+ id: node2.slug,
36252
+ title: node2.title,
36253
+ type: node2.type,
36254
+ tags: node2.tags,
36255
+ sources: [...node2.sources],
36256
+ updated: now.toISOString().slice(0, 10),
36257
+ ...node2.context_sources !== undefined ? { context_sources: [...node2.context_sources] } : {},
36258
+ ...node2.summary !== undefined ? { summary: node2.summary } : {},
36259
+ ...node2.domain_gate !== undefined ? { domain_gate: node2.domain_gate } : {}
36260
+ },
36261
+ body: "",
36262
+ sections: [],
36263
+ containsList: children.map((child) => ({
36264
+ slug: child.slug,
36265
+ title: child.title,
36266
+ href: `../${child.type}/${child.slug}.md`
36267
+ })),
36268
+ children: []
36269
+ }
36270
+ });
36271
+ created += 1;
36272
+ }
36273
+ return created;
36274
+ }
36275
+ async function ensureFinalizedNoWritePlaceholders(ctxDir, now) {
36276
+ const ownership = await readCurrentSourceOwnership(ctxDir);
36277
+ if (!ownership)
36278
+ return 0;
36279
+ const nodes = ownership.nodes ?? [];
36280
+ let created = 0;
36281
+ for (const node2 of nodes) {
36282
+ if (!isExplicitNoWriteNode(node2))
36283
+ continue;
36284
+ if (node2.sources.length > 0)
36285
+ continue;
36286
+ if (!hasNoWritePlaceholderSupport(ownership, node2))
36287
+ continue;
36288
+ if (existsSync43(finalizedNodePath(ctxDir, node2)))
36289
+ continue;
36290
+ const children = finalizedContainerChildren(nodes, node2);
36291
+ await mdriveNodeCreate({
36292
+ ctxDir,
36293
+ input: {
36294
+ node: {
36295
+ id: node2.slug,
36296
+ title: node2.title,
36297
+ type: node2.type,
36298
+ tags: [...node2.tags],
36299
+ sources: [],
36300
+ updated: now.toISOString().slice(0, 10),
36301
+ ...node2.context_sources !== undefined ? { context_sources: [...node2.context_sources] } : {},
36302
+ ...node2.summary !== undefined ? { summary: node2.summary } : {},
36303
+ ...node2.language !== undefined ? { language: node2.language } : {}
36304
+ },
36305
+ body: "",
36306
+ sections: [],
36307
+ containsList: children.map((child) => ({
36308
+ slug: child.slug,
36309
+ title: child.title,
36310
+ href: `../${child.type}/${child.slug}.md`
36311
+ })),
36312
+ children: []
36313
+ }
36314
+ });
36315
+ created += 1;
36316
+ }
36317
+ return created;
36318
+ }
36319
+ var init_compileCloseFinalizedNodes = __esm(() => {
36320
+ init_node2();
36321
+ init_sourceOwnership();
36322
+ });
36323
+
36324
+ // src/workflow/compileClose.ts
36325
+ import { existsSync as existsSync44 } from "node:fs";
36326
+ import { readFile as readFile51, readdir as readdir19, rm as rm13 } from "node:fs/promises";
36327
+ import { join as join53 } from "node:path";
36170
36328
  function knowledgePath(ctxDir, name) {
36171
- return join52(ctxDir, "knowledge", name);
36329
+ return join53(ctxDir, "knowledge", name);
36172
36330
  }
36173
36331
  async function snapshotFile(path8) {
36174
- return existsSync43(path8) ? { path: path8, content: await readFile51(path8, "utf8") } : { path: path8 };
36332
+ return existsSync44(path8) ? { path: path8, content: await readFile51(path8, "utf8") } : { path: path8 };
36175
36333
  }
36176
36334
  async function snapshotCompileCloseFiles(ctxDir) {
36177
36335
  const paths = new Set([
36178
36336
  knowledgePath(ctxDir, "_index.md"),
36179
36337
  knowledgePath(ctxDir, "changelog.md"),
36180
- join52(ctxDir, "knowledge", "_edges.yaml")
36338
+ join53(ctxDir, "knowledge", "_edges.yaml")
36181
36339
  ]);
36182
36340
  for (const type of NODE_TYPES) {
36183
- const dir = join52(ctxDir, "knowledge", type);
36184
- if (!existsSync43(dir))
36341
+ const dir = join53(ctxDir, "knowledge", type);
36342
+ if (!existsSync44(dir))
36185
36343
  continue;
36186
36344
  const entries = await readdir19(dir, { withFileTypes: true });
36187
36345
  for (const entry of entries) {
36188
36346
  if (entry.isFile() && entry.name.endsWith(".md")) {
36189
- paths.add(join52(dir, entry.name));
36347
+ paths.add(join53(dir, entry.name));
36190
36348
  }
36191
36349
  }
36192
36350
  }
@@ -36205,7 +36363,7 @@ async function restoreCompileCloseSnapshot(snapshot) {
36205
36363
  }
36206
36364
  async function readLastCompileAt2(ctxDir) {
36207
36365
  const changelog = knowledgePath(ctxDir, "changelog.md");
36208
- if (!existsSync43(changelog))
36366
+ if (!existsSync44(changelog))
36209
36367
  return null;
36210
36368
  const lines = (await readFile51(changelog, "utf8")).split(`
36211
36369
  `).filter((line) => line.includes("[compile]"));
@@ -36242,7 +36400,7 @@ async function compileLedgerAppliedNodesAfter(ctxDir, timestamp) {
36242
36400
  }
36243
36401
  async function compileDraftScratchEntries(ctxDir) {
36244
36402
  const outputDir = workflowOutputDir(ctxDir, "compile");
36245
- if (!existsSync43(outputDir))
36403
+ if (!existsSync44(outputDir))
36246
36404
  return [];
36247
36405
  const entries = await readdir19(outputDir, { withFileTypes: true });
36248
36406
  const out2 = [];
@@ -36251,7 +36409,7 @@ async function compileDraftScratchEntries(ctxDir) {
36251
36409
  continue;
36252
36410
  const match = /^compile\.(.+)\.draft\.(?:json|ya?ml)$/u.exec(entry.name);
36253
36411
  if (match?.[1])
36254
- out2.push({ slug: match[1], path: join52(outputDir, entry.name) });
36412
+ out2.push({ slug: match[1], path: join53(outputDir, entry.name) });
36255
36413
  }
36256
36414
  return out2;
36257
36415
  }
@@ -36331,54 +36489,11 @@ async function assertCompileCloseVerifyOk(ctxDir, allowedActiveEmptySlugs = new
36331
36489
  });
36332
36490
  }
36333
36491
  }
36334
- function finalizedContainerChildren(nodes, parent) {
36335
- if (parent.type !== "domain")
36336
- return [];
36337
- return nodes.filter((node2) => node2.contains_parent === parent.slug).sort((left, right) => left.slug.localeCompare(right.slug));
36338
- }
36339
- async function ensureFinalizedContainerDomains(ctxDir, now) {
36340
- const ownership = await readCurrentSourceOwnership(ctxDir);
36341
- const nodes = ownership?.nodes ?? [];
36342
- let created = 0;
36343
- for (const node2 of nodes) {
36344
- const children = finalizedContainerChildren(nodes, node2);
36345
- if (children.length === 0)
36346
- continue;
36347
- if (existsSync43(join52(ctxDir, "knowledge", "domain", `${node2.slug}.md`)))
36348
- continue;
36349
- await mdriveNodeCreate({
36350
- ctxDir,
36351
- input: {
36352
- node: {
36353
- id: node2.slug,
36354
- title: node2.title,
36355
- type: node2.type,
36356
- tags: node2.tags,
36357
- sources: [...node2.sources],
36358
- updated: now.toISOString().slice(0, 10),
36359
- ...node2.context_sources !== undefined ? { context_sources: [...node2.context_sources] } : {},
36360
- ...node2.summary !== undefined ? { summary: node2.summary } : {},
36361
- ...node2.domain_gate !== undefined ? { domain_gate: node2.domain_gate } : {}
36362
- },
36363
- body: "",
36364
- sections: [],
36365
- containsList: children.map((child) => ({
36366
- slug: child.slug,
36367
- title: child.title,
36368
- href: `../${child.type}/${child.slug}.md`
36369
- })),
36370
- children: []
36371
- }
36372
- });
36373
- created += 1;
36374
- }
36375
- return created;
36376
- }
36377
36492
  async function assertFinalizedKnowledgeNodesMaterialized(ctxDir) {
36378
36493
  const ownership = await readCurrentSourceOwnership(ctxDir);
36379
36494
  if (!ownership)
36380
36495
  return;
36381
- const missing = (ownership.nodes ?? []).filter((node2) => !existsSync43(join52(ctxDir, "knowledge", node2.type, `${node2.slug}.md`)));
36496
+ const missing = (ownership.nodes ?? []).filter((node2) => !existsSync44(finalizedNodePath(ctxDir, node2)));
36382
36497
  if (missing.length === 0)
36383
36498
  return;
36384
36499
  const activeSlugs = finalizedSlugSet(ownership);
@@ -36522,8 +36637,8 @@ async function compileClose(options) {
36522
36637
  }
36523
36638
  }
36524
36639
  async function compileCloseInner(options) {
36525
- const rebuiltIndex = !existsSync43(knowledgePath(options.ctxDir, "_index.md"));
36526
- const rebuiltChangelog = !existsSync43(knowledgePath(options.ctxDir, "changelog.md"));
36640
+ const rebuiltIndex = !existsSync44(knowledgePath(options.ctxDir, "_index.md"));
36641
+ const rebuiltChangelog = !existsSync44(knowledgePath(options.ctxDir, "changelog.md"));
36527
36642
  const now = options.now ?? new Date;
36528
36643
  const sourcesFile = await loadSources(options.ctxDir);
36529
36644
  const workspaceRoot = workspaceRootFromCtxDir(options.ctxDir);
@@ -36551,6 +36666,7 @@ async function compileCloseInner(options) {
36551
36666
  const coverageStatus = await readCoverageWorkspaceStatus(options.ctxDir);
36552
36667
  const retractedNodes = await retractRemovedFinalizedNodes(options.ctxDir);
36553
36668
  const createdContainerDomains = await ensureFinalizedContainerDomains(options.ctxDir, now);
36669
+ await ensureFinalizedNoWritePlaceholders(options.ctxDir, now);
36554
36670
  await assertFinalizedKnowledgeNodesMaterialized(options.ctxDir);
36555
36671
  await projectFinalizedGraph(options.ctxDir);
36556
36672
  const stats = await mdriveWorkspaceStats({ ctxDir: options.ctxDir });
@@ -36655,21 +36771,21 @@ function coverageDebtItems(status) {
36655
36771
  }));
36656
36772
  }
36657
36773
  async function readWorkflowPayloadFiles(ctxDir, payloadBaseName) {
36658
- const root = join52(ctxDir, "output", "workflows");
36659
- if (!existsSync43(root))
36774
+ const root = join53(ctxDir, "output", "workflows");
36775
+ if (!existsSync44(root))
36660
36776
  return [];
36661
36777
  const out2 = [];
36662
36778
  for (const workflowEntry of await readdir19(root, { withFileTypes: true })) {
36663
36779
  if (!workflowEntry.isDirectory())
36664
36780
  continue;
36665
- const workflowDir = join52(root, workflowEntry.name);
36781
+ const workflowDir = join53(root, workflowEntry.name);
36666
36782
  for (const scopeEntry of await readdir19(workflowDir, { withFileTypes: true }).catch(() => [])) {
36667
36783
  if (!scopeEntry.isDirectory())
36668
36784
  continue;
36669
- const scopeDir = join52(workflowDir, scopeEntry.name);
36785
+ const scopeDir = join53(workflowDir, scopeEntry.name);
36670
36786
  for (const format of ["json", "yaml"]) {
36671
- const path8 = join52(scopeDir, `${payloadBaseName}.${format}`);
36672
- if (!existsSync43(path8))
36787
+ const path8 = join53(scopeDir, `${payloadBaseName}.${format}`);
36788
+ if (!existsSync44(path8))
36673
36789
  continue;
36674
36790
  const body2 = await readFile51(path8, "utf8");
36675
36791
  out2.push(format === "json" ? JSON.parse(body2) : import_yaml27.default.parse(body2));
@@ -36781,6 +36897,7 @@ var init_compileClose = __esm(() => {
36781
36897
  init_workflowPayloadStore();
36782
36898
  init_edge2();
36783
36899
  init_graphEdges();
36900
+ init_compileCloseFinalizedNodes();
36784
36901
  import_yaml27 = __toESM(require_dist(), 1);
36785
36902
  });
36786
36903
 
@@ -37643,9 +37760,9 @@ var exports_prepareCompile = {};
37643
37760
  __export(exports_prepareCompile, {
37644
37761
  prepareCompileReconcileContext: () => prepareCompileReconcileContext
37645
37762
  });
37646
- import { existsSync as existsSync44 } from "node:fs";
37763
+ import { existsSync as existsSync45 } from "node:fs";
37647
37764
  import { readdir as readdir20, readFile as readFile52 } from "node:fs/promises";
37648
- import { join as join53 } from "node:path";
37765
+ import { join as join54 } from "node:path";
37649
37766
  function targetSectionSourceRef(node2, sectionId) {
37650
37767
  if (!sectionId)
37651
37768
  return;
@@ -37943,7 +38060,7 @@ function isSafeRelativePath(path8) {
37943
38060
  return path8.trim().length > 0 && !path8.startsWith("/") && !path8.split(/[\\/]+/u).includes("..");
37944
38061
  }
37945
38062
  async function readDropArchiveManifest2(path8) {
37946
- if (!existsSync44(path8))
38063
+ if (!existsSync45(path8))
37947
38064
  return null;
37948
38065
  try {
37949
38066
  const parsed = import_yaml28.default.parse(await readFile52(path8, "utf8"));
@@ -37988,27 +38105,27 @@ async function restoredArchiveCandidatesForNode(ctxDir, targetNode) {
37988
38105
  const restoredSourceIds = new Set(sourcesFile.sources.filter((source2) => source2.status === "active" && typeof source2.restored_at === "string").map((source2) => source2.id));
37989
38106
  if (restoredSourceIds.size === 0)
37990
38107
  return [];
37991
- const sourcesRoot = join53(ctxDir, "archive", "sources");
37992
- if (!existsSync44(sourcesRoot))
38108
+ const sourcesRoot = join54(ctxDir, "archive", "sources");
38109
+ if (!existsSync45(sourcesRoot))
37993
38110
  return [];
37994
38111
  const out2 = [];
37995
38112
  for (const sourceDir of await readdir20(sourcesRoot, { withFileTypes: true })) {
37996
38113
  if (!sourceDir.isDirectory())
37997
38114
  continue;
37998
- const sourceRoot = join53(sourcesRoot, sourceDir.name);
38115
+ const sourceRoot = join54(sourcesRoot, sourceDir.name);
37999
38116
  for (const archiveDir of await readdir20(sourceRoot, { withFileTypes: true })) {
38000
38117
  if (!archiveDir.isDirectory())
38001
38118
  continue;
38002
38119
  const archivePath = `archive/sources/${sourceDir.name}/${archiveDir.name}`;
38003
- const manifest = await readDropArchiveManifest2(join53(ctxDir, archivePath, "manifest.yaml"));
38120
+ const manifest = await readDropArchiveManifest2(join54(ctxDir, archivePath, "manifest.yaml"));
38004
38121
  if (!manifest || !restoredSourceIds.has(manifest.source_id))
38005
38122
  continue;
38006
38123
  const knowledgeBefore = Array.isArray(manifest.knowledge_before) ? manifest.knowledge_before : [];
38007
38124
  for (const relPath of knowledgeBefore) {
38008
38125
  if (!isSafeRelativePath(relPath))
38009
38126
  continue;
38010
- const absPath = join53(ctxDir, relPath);
38011
- if (!existsSync44(absPath))
38127
+ const absPath = join54(ctxDir, relPath);
38128
+ if (!existsSync45(absPath))
38012
38129
  continue;
38013
38130
  let parsed;
38014
38131
  try {
@@ -38711,8 +38828,8 @@ var init_prepare = __esm(() => {
38711
38828
  });
38712
38829
 
38713
38830
  // src/cli.ts
38714
- import { existsSync as existsSync48, readFileSync as readFileSync3, realpathSync } from "node:fs";
38715
- import { dirname as dirname28, join as join62 } from "node:path";
38831
+ import { existsSync as existsSync49, readFileSync as readFileSync3, realpathSync } from "node:fs";
38832
+ import { dirname as dirname28, join as join63 } from "node:path";
38716
38833
  import { fileURLToPath as fileURLToPath4, pathToFileURL as pathToFileURL3 } from "node:url";
38717
38834
 
38718
38835
  // ../../node_modules/.bun/commander@11.1.0/node_modules/commander/esm.mjs
@@ -38882,6 +38999,7 @@ This directory is the C4A data root for a knowledge workspace.
38882
38999
  - Do not pipe or wrap \`context ... --format json\` output through \`python3\`, \`node\`, \`jq\`, \`sed\`, \`cat\`, \`2>&1\`, \`|| echo\`, or similar shell helpers. Run the \`context\` command directly and consume its complete stdout.
38883
39000
  - Schema discovery must also go through direct \`context\` commands. Schema commands support \`--format text|json|yaml\`; use readable YAML/text while drafting and JSON when a caller needs machine parsing. Inspect the schema \`enums\` field for allowed tags/actions instead of inventing labels such as \`platform\` or \`tool\`.
38884
39001
  - When passing large JSON / YAML / Markdown bodies to the CLI, feed stdin directly into the \`context\` command with a heredoc, for example \`context align --finalize - --digest <segments-digest> <<'JSON'\`. Do not pipe a heredoc through another command, and never redirect heredocs to workspace files.
39002
+ - For large \`context align --finalize\` payloads, use \`block_ownership_defaults[]\` as a compact source-level ownership rule instead of generating temporary JSON files or enumerating hundreds of block rows. Defaults must still declare an explicit \`source_id\`, \`ownership_role\`, owner/visibility fields, and reason when needed; use \`block_ownership[]\` for block-level exceptions. Defaults are a compression mechanism, not permission to assign mixed-source evidence to one owner without review.
38885
39003
  - In Claude Code, direct heredoc commands such as \`context compile --draft <slug> --input - --plan <<'JSON'\` may still prompt for permission when only subcommand-specific allow rules are configured. If that happens, ask the user before adding the scoped allow rule \`Bash(context:*)\` to the project \`.claude/settings.local.json\`; do not enable \`bypassPermissions\`.
38886
39004
  - Never hand-edit \`raw/\`, \`raw/_sources.yaml\`, \`archive/\`, \`knowledge/\`, \`knowledge/_index.md\`, or \`knowledge/changelog.md\`. Maintain them through \`context\` CLI commands and \`/context:*\` workflows.
38887
39005
  - \`decisions/\` stores CLI-managed semantic reconciliation decisions. Do not directly read, search, or write \`decisions/semantic.yaml\`; use \`context reconcile prepare|review|apply|record\`.
@@ -47457,8 +47575,8 @@ var DOCTOR_ACTIONS = {
47457
47575
  "coverage-high-signal-unresolved": {
47458
47576
  hintCode: "coverage-high-signal-unresolved",
47459
47577
  message: (count) => `${count} high-signal coverage issue(s) must be disposed before compile close.`,
47460
- next_action: "Use context status --format json to find coverage.candidates[].node_slug, then open the matching node-scoped coverage payload with context workflow show and continue /context:compile.",
47461
- command: "context schema coverage-disposition"
47578
+ next_action: "Use context status --format json to find coverage.candidates[].node_slug, read the matching node-scoped coverage payload, then use --coverage-skip-unresolved when all unresolved candidates are intentionally excluded.",
47579
+ command: 'context compile --coverage-skip-unresolved --coverage-disposition-node <slug> --payload-digest <digest> --reason "<reason>"'
47462
47580
  },
47463
47581
  "coverage-diagnostics-failed": {
47464
47582
  hintCode: "coverage-diagnostics-failed",
@@ -49722,7 +49840,7 @@ function reviewAgentHints(input) {
49722
49840
  code: "source-support-repair",
49723
49841
  severity: "error",
49724
49842
  message: `${sourceSupportIssueCount} decision(s) cite raw text that does not support the proposed content.`,
49725
- next_action: "Do not accept defaults for unsupported items. Inspect the prepared item with --view issues, then fix the draft by narrowing content, choosing source_support.evidence_block_source_ref/candidates, or splitting the action. Use draft-patch when a draft session digest exists; use source resolve-ref only when you have an exact quote but not the citation token.",
49843
+ next_action: "Do not accept defaults for unsupported items. Missing hard facts mean the cited raw text does not cover the proposed content; narrow content, choose source_support.evidence_block_source_ref/candidates, or split the action. Low lexical overlap without missing hard facts is reported as a warning.",
49726
49844
  command: "context workflow show --payload prepare --scope <scope-id> --digest <prepare-digest> --view issues --status unsupported --unwrap --format json"
49727
49845
  });
49728
49846
  }
@@ -49799,13 +49917,13 @@ function reviewAgentHints(input) {
49799
49917
  schema: "context schema semantic-decisions --format yaml"
49800
49918
  });
49801
49919
  }
49802
- const weakSupportCount = input.questions.filter((question) => question.type === "support_confirmation").length;
49920
+ const weakSupportCount = input.issues.filter((issue) => issue.code === "weak-source-support").length + input.questions.filter((question) => question.type === "support_confirmation").length;
49803
49921
  if (weakSupportCount > 0) {
49804
49922
  hints.push({
49805
- code: "weak-source-support-needs-confirmation",
49923
+ code: "weak-source-support-warning",
49806
49924
  severity: "warning",
49807
49925
  message: `${weakSupportCount} item(s) are only weakly supported by the cited evidence.`,
49808
- next_action: "Ask the user/tester to confirm the summary/compression or choose/split source_ref/source_refs before applying."
49926
+ next_action: "Proceed only if the weak summary/compression is acceptable for this workflow; otherwise narrow content or choose/split source_ref/source_refs."
49809
49927
  });
49810
49928
  }
49811
49929
  const exampleDetailQuestionCount = input.questions.filter((question) => question.type === "example_detail_preservation").length;
@@ -49814,7 +49932,7 @@ function reviewAgentHints(input) {
49814
49932
  code: "example-detail-preservation",
49815
49933
  severity: "warning",
49816
49934
  message: `${exampleDetailQuestionCount} example decision(s) cite code/config/command evidence without preserving it in detail.`,
49817
- next_action: "Move the relevant fenced block into proposed.detail and rerun review, or ask the user to confirm prose-only summary is acceptable."
49935
+ next_action: "Move the relevant fenced block into proposed.detail and rerun review, or ask the user to confirm prose-only summary is acceptable. Auto mode or general permission to continue is not confirmation."
49818
49936
  });
49819
49937
  }
49820
49938
  const urlReferenceQuestionCount = input.questions.filter((question) => question.type === "url_reference_preservation").length;
@@ -49823,7 +49941,7 @@ function reviewAgentHints(input) {
49823
49941
  code: "url-reference-preservation",
49824
49942
  severity: "warning",
49825
49943
  message: `${urlReferenceQuestionCount} decision(s) cite URL evidence without preserving the referenced link.`,
49826
- next_action: "Move the relevant URL into proposed.detail and rerun review, or ask the user to confirm the link may be omitted."
49944
+ next_action: "Move the relevant URL into proposed.detail and rerun review, or ask the user to confirm the link may be omitted. Auto mode or general permission to continue is not confirmation."
49827
49945
  });
49828
49946
  }
49829
49947
  const unresolvedQuestionCount = input.questions.length;
@@ -49893,7 +50011,7 @@ function urlReferencePreservationQuestion(input) {
49893
50011
  ...input.decision.proposed?.content !== undefined ? { proposed_content: input.decision.proposed.content } : {},
49894
50012
  cited_urls: diagnostic.citedUrls,
49895
50013
  missing_urls: diagnostic.missingUrls,
49896
- prompt: "The cited raw evidence includes URL(s), but this document/link/reference claim does not preserve them in proposed content or detail. " + "Add the relevant URL(s) to proposed.detail and rerun review, or ask the user to confirm that omitting the link is acceptable and mark the decision decided_by: user."
50014
+ prompt: "The cited raw evidence includes URL(s), but this document/link/reference claim does not preserve them in proposed content or detail. " + "Add the relevant URL(s) to proposed.detail and rerun review, or ask the user to confirm that omitting the link is acceptable and mark the decision decided_by: user. Auto mode or general permission to continue is not confirmation."
49897
50015
  };
49898
50016
  }
49899
50017
  function urlReferencePreservationQuestions(input) {
@@ -50103,7 +50221,7 @@ function semanticDecisionSchemaErrorReview(input) {
50103
50221
  }
50104
50222
 
50105
50223
  // src/reconcile/review.ts
50106
- var FENCED_CODE_RE3 = /(^|\n)\s*(```|~~~)/u;
50224
+ var FENCED_CODE_RE2 = /(^|\n)\s*(```|~~~)/u;
50107
50225
  function isRecord21(value) {
50108
50226
  return typeof value === "object" && value !== null && !Array.isArray(value);
50109
50227
  }
@@ -50371,6 +50489,11 @@ function actionNeedsSourceSupport2(action) {
50371
50489
  function actionRequiresStrictSourceSupport(action) {
50372
50490
  return action === "merge_update" || action === "supersede" || action === "reanchor" || action === "split_then_reanchor";
50373
50491
  }
50492
+ function sourceRefChangedFromPreparedSupport(input) {
50493
+ const proposedSourceRef = input.decision.proposed?.source_ref;
50494
+ const preparedSourceRef = input.item?.source_support?.source_ref;
50495
+ return typeof proposedSourceRef === "string" && typeof preparedSourceRef === "string" && proposedSourceRef !== preparedSourceRef;
50496
+ }
50374
50497
  function supportKind(decision, item) {
50375
50498
  if (typeof decision.proposed?.kind === "string")
50376
50499
  return decision.proposed.kind;
@@ -50396,21 +50519,14 @@ function sourceSupportIssue(input) {
50396
50519
  ...splitCandidateCount > 1 ? { code: "source-support-split-by-evidence-blocks" } : {}
50397
50520
  };
50398
50521
  }
50399
- function weakSupportQuestion(input) {
50400
- const sourceAlias2 = typeof input.decision.proposed?.source_ref === "string" ? /^(src-\d+)#/u.exec(input.decision.proposed.source_ref)?.[1] : undefined;
50401
- const node2 = input.decision.target?.node ?? input.item?.target?.node;
50402
- const groupKey = node2 !== undefined && sourceAlias2 !== undefined ? `support:${node2}:${sourceAlias2}` : undefined;
50522
+ function weakSupportWarning(input) {
50403
50523
  return {
50404
- question_id: `q-${String(input.questionIndex + 1).padStart(3, "0")}`,
50524
+ path: `${input.decision.item_id}.proposed.source_ref`,
50405
50525
  item_id: input.decision.item_id,
50406
- type: "support_confirmation",
50407
- ...groupKey !== undefined ? {
50408
- group_key: groupKey,
50409
- group_reason: "same Node/source weak summary support; show each cited evidence boundary before asking once"
50410
- } : {},
50411
- ...input.summary !== undefined ? { candidate_summary: { active: input.summary.active, archive: input.summary.archive } } : {},
50412
- ...input.decision.proposed?.content !== undefined ? { proposed_content: input.decision.proposed.content } : {},
50413
- prompt: "The proposed new knowledge is only weakly supported by the cited raw text. " + `${formatSupportDiagnostic(input.diagnostic)}. ` + "Ask the user whether this is an acceptable summary/compression; if confirmed, regenerate the final decision with decided_by: user."
50526
+ severity: "warning",
50527
+ code: "weak-source-support",
50528
+ message: `proposed keep_separate is weakly supported by its cited raw text: ${formatSupportDiagnostic(input.diagnostic)}.`,
50529
+ next_action: "Review the cited evidence if the summary looks surprising; missing hard facts still remain errors."
50414
50530
  };
50415
50531
  }
50416
50532
  function citedTextForDecision2(decision, item) {
@@ -50431,9 +50547,9 @@ function needsExampleDetailPreservationQuestion(input) {
50431
50547
  return false;
50432
50548
  if (supportKind(input.decision, input.item) !== "example")
50433
50549
  return false;
50434
- if (!FENCED_CODE_RE3.test(citedTextForDecision2(input.decision, input.item)))
50550
+ if (!FENCED_CODE_RE2.test(citedTextForDecision2(input.decision, input.item)))
50435
50551
  return false;
50436
- return !FENCED_CODE_RE3.test(input.decision.proposed?.detail ?? "");
50552
+ return !FENCED_CODE_RE2.test(input.decision.proposed?.detail ?? "");
50437
50553
  }
50438
50554
  function exampleDetailPreservationQuestion(input) {
50439
50555
  return {
@@ -50442,7 +50558,7 @@ function exampleDetailPreservationQuestion(input) {
50442
50558
  type: "example_detail_preservation",
50443
50559
  ...input.summary !== undefined ? { candidate_summary: { active: input.summary.active, archive: input.summary.archive } } : {},
50444
50560
  ...input.decision.proposed?.content !== undefined ? { proposed_content: input.decision.proposed.content } : {},
50445
- prompt: "The cited raw evidence contains a fenced code/config/command block, but this example decision does not preserve it in proposed.detail. " + "Add the relevant fenced block to proposed.detail and rerun review, or ask the user to confirm that a prose-only summary is acceptable and mark the decision decided_by: user."
50561
+ prompt: "The cited raw evidence contains a fenced code/config/command block, but this example decision does not preserve it in proposed.detail. " + "Add the relevant fenced block to proposed.detail and rerun review, or ask the user to confirm that a prose-only summary is acceptable and mark the decision decided_by: user. Auto mode or general permission to continue is not confirmation."
50446
50562
  };
50447
50563
  }
50448
50564
  function itemRequiresDecision(item) {
@@ -50515,15 +50631,13 @@ function reviewSemanticDecisions(input) {
50515
50631
  }
50516
50632
  for (const prior of priorResults) {
50517
50633
  const diagnostic = supportDiagnosticFromItem({ decision: prior.decision, item: prior.item });
50518
- if (diagnostic?.verdict === "weak" && prior.decision.action === "keep_separate" && prior.decision.decided_by !== "user") {
50519
- questions.push(weakSupportQuestion({
50634
+ const changedSourceRef = sourceRefChangedFromPreparedSupport({ decision: prior.decision, item: prior.item });
50635
+ if (diagnostic?.verdict === "weak" && prior.decision.action === "keep_separate" && !changedSourceRef) {
50636
+ issues.push(weakSupportWarning({
50520
50637
  decision: prior.decision,
50521
- item: prior.item,
50522
- questionIndex: questions.length,
50523
- summary: candidateSummaryByItem.get(prior.decision.item_id),
50524
50638
  diagnostic
50525
50639
  }));
50526
- } else if (diagnostic?.verdict === "unsupported" || diagnostic?.verdict === "weak" && !(prior.decision.action === "keep_separate" && prior.decision.decided_by === "user")) {
50640
+ } else if (diagnostic?.verdict === "unsupported" || changedSourceRef && diagnostic?.verdict === "weak" || diagnostic?.verdict === "weak" && !(prior.decision.action === "keep_separate" && prior.decision.decided_by === "user")) {
50527
50641
  issues.push(sourceSupportIssue({ decision: prior.decision, diagnostic, support: prior.item?.source_support }));
50528
50642
  }
50529
50643
  }
@@ -50651,6 +50765,7 @@ async function reviewSemanticDecisionsWithWorkspace(input) {
50651
50765
  if (node2 === undefined)
50652
50766
  continue;
50653
50767
  try {
50768
+ const changedPreparedSourceRef = typeof item?.source_support?.source_ref === "string" && item.source_support.source_ref !== sourceRef;
50654
50769
  const result = await diagnoseSectionSourceSupport({
50655
50770
  ctxDir: input.ctxDir,
50656
50771
  nodeSources: node2.parsed.node.sources,
@@ -50663,13 +50778,21 @@ async function reviewSemanticDecisionsWithWorkspace(input) {
50663
50778
  source_ref: sourceRef
50664
50779
  }
50665
50780
  });
50666
- item.source_support = sourceSupportFromDiagnostic({
50781
+ const diagnostic = changedPreparedSourceRef && result.diagnostic.verdict === "weak" ? { ...result.diagnostic, verdict: "unsupported" } : result.diagnostic;
50782
+ const sourceSupport = sourceSupportFromDiagnostic({
50667
50783
  sourceRef: result.sourceRef,
50668
50784
  citedText: result.citedText,
50669
- diagnostic: result.diagnostic,
50785
+ diagnostic,
50670
50786
  ...result.evidenceBlock !== undefined ? { evidenceBlock: result.evidenceBlock } : {},
50671
50787
  ...result.evidenceBlockCandidates !== undefined ? { evidenceBlockCandidates: result.evidenceBlockCandidates } : {}
50672
50788
  });
50789
+ if (changedPreparedSourceRef && result.diagnostic.verdict === "weak") {
50790
+ const withoutCitedText = { ...sourceSupport };
50791
+ delete withoutCitedText.cited_text;
50792
+ item.source_support = withoutCitedText;
50793
+ } else {
50794
+ item.source_support = sourceSupport;
50795
+ }
50673
50796
  } catch (error) {
50674
50797
  item.source_support = {
50675
50798
  source_ref: sourceRef,
@@ -50755,10 +50878,10 @@ init_compileNode();
50755
50878
  init_coverage();
50756
50879
  init_evidence();
50757
50880
  init_ledger();
50758
- import { existsSync as existsSync45 } from "node:fs";
50881
+ import { existsSync as existsSync46 } from "node:fs";
50759
50882
  import { cp as cp3, mkdir as mkdir25, mkdtemp as mkdtemp2, rm as rm14 } from "node:fs/promises";
50760
50883
  import { tmpdir as tmpdir2 } from "node:os";
50761
- import { dirname as dirname22, join as join54 } from "node:path";
50884
+ import { dirname as dirname22, join as join55 } from "node:path";
50762
50885
  init_sourceSupport();
50763
50886
  init_locatedNodeSources();
50764
50887
  init_types3();
@@ -50848,13 +50971,13 @@ async function propagateOmitCoverageSkips(input) {
50848
50971
  }
50849
50972
  }
50850
50973
  async function createApplyRollbackSnapshot(ctxDir) {
50851
- const tmpDir = await mkdtemp2(join54(tmpdir2(), "c4a-reconcile-apply-"));
50852
- const knowledgePath2 = join54(ctxDir, "knowledge");
50974
+ const tmpDir = await mkdtemp2(join55(tmpdir2(), "c4a-reconcile-apply-"));
50975
+ const knowledgePath2 = join55(ctxDir, "knowledge");
50853
50976
  const ledgerPath = semanticLedgerPath2(ctxDir);
50854
- const knowledgeBackup = join54(tmpDir, "knowledge");
50855
- const ledgerBackup = join54(tmpDir, "semantic.yaml");
50856
- const knowledgeExisted = existsSync45(knowledgePath2);
50857
- const ledgerExisted = existsSync45(ledgerPath);
50977
+ const knowledgeBackup = join55(tmpDir, "knowledge");
50978
+ const ledgerBackup = join55(tmpDir, "semantic.yaml");
50979
+ const knowledgeExisted = existsSync46(knowledgePath2);
50980
+ const ledgerExisted = existsSync46(ledgerPath);
50858
50981
  if (knowledgeExisted)
50859
50982
  await cp3(knowledgePath2, knowledgeBackup, { recursive: true });
50860
50983
  if (ledgerExisted)
@@ -50862,7 +50985,7 @@ async function createApplyRollbackSnapshot(ctxDir) {
50862
50985
  return { ctxDir, tmpDir, knowledgeExisted, ledgerExisted, knowledgeBackup, ledgerBackup };
50863
50986
  }
50864
50987
  async function restoreApplyRollbackSnapshot(snapshot) {
50865
- const knowledgePath2 = join54(snapshot.ctxDir, "knowledge");
50988
+ const knowledgePath2 = join55(snapshot.ctxDir, "knowledge");
50866
50989
  if (snapshot.knowledgeExisted) {
50867
50990
  await rm14(knowledgePath2, { recursive: true, force: true });
50868
50991
  await cp3(snapshot.knowledgeBackup, knowledgePath2, { recursive: true });
@@ -52364,9 +52487,9 @@ init_edge2();
52364
52487
  // src/lib/glossary.ts
52365
52488
  init_knowledge();
52366
52489
  init_nodeParser();
52367
- import { existsSync as existsSync46 } from "node:fs";
52490
+ import { existsSync as existsSync47 } from "node:fs";
52368
52491
  import { readdir as readdir21, readFile as readFile55 } from "node:fs/promises";
52369
- import { join as join55 } from "node:path";
52492
+ import { join as join56 } from "node:path";
52370
52493
  function collectGlossaryFromParsedTree(parsed, glossary, allowedTypes) {
52371
52494
  const type = parsed.node.type;
52372
52495
  if (allowedTypes.has(type)) {
@@ -52389,16 +52512,16 @@ function normalizeGlossaryTerm(value) {
52389
52512
  return value.normalize("NFC").trim().toLowerCase();
52390
52513
  }
52391
52514
  function resolveKnowledgeRoot(path8) {
52392
- if (existsSync46(join55(path8, "knowledge"))) {
52393
- return join55(path8, "knowledge");
52515
+ if (existsSync47(join56(path8, "knowledge"))) {
52516
+ return join56(path8, "knowledge");
52394
52517
  }
52395
52518
  return path8;
52396
52519
  }
52397
52520
  async function readGlossaryFiles(dir) {
52398
- if (!existsSync46(dir))
52521
+ if (!existsSync47(dir))
52399
52522
  return [];
52400
52523
  const entries = await readdir21(dir, { withFileTypes: true });
52401
- return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md") && entry.name !== "_index.md" && entry.name !== "changelog.md").map((entry) => join55(dir, entry.name));
52524
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md") && entry.name !== "_index.md" && entry.name !== "changelog.md").map((entry) => join56(dir, entry.name));
52402
52525
  }
52403
52526
  async function buildGlossary(path8, options = {}) {
52404
52527
  const knowledgeRoot2 = resolveKnowledgeRoot(path8);
@@ -52406,7 +52529,7 @@ async function buildGlossary(path8, options = {}) {
52406
52529
  const allowedTypes = new Set(targets);
52407
52530
  const glossary = [];
52408
52531
  for (const type of targets) {
52409
- const files = await readGlossaryFiles(join55(knowledgeRoot2, type));
52532
+ const files = await readGlossaryFiles(join56(knowledgeRoot2, type));
52410
52533
  for (const file of files) {
52411
52534
  const parsed = parseNodeMarkdown(await readFile55(file, "utf8"));
52412
52535
  if (parsed.node.type !== type)
@@ -53020,9 +53143,9 @@ function registerMdriveCommands(program2) {
53020
53143
  init_cliFeedback();
53021
53144
  init_errors();
53022
53145
  var import_yaml32 = __toESM(require_dist(), 1);
53023
- import { existsSync as existsSync47 } from "node:fs";
53146
+ import { existsSync as existsSync48 } from "node:fs";
53024
53147
  import { lstat as lstat2, mkdir as mkdir28, readFile as readFile58, readdir as readdir22, realpath as realpath3, rename as rename7, writeFile as writeFile21 } from "node:fs/promises";
53025
- import { dirname as dirname25, isAbsolute as isAbsolute6, join as join56, relative as relative12 } from "node:path";
53148
+ import { dirname as dirname25, isAbsolute as isAbsolute6, join as join57, relative as relative12 } from "node:path";
53026
53149
  init_workspaceLayout();
53027
53150
  init_section();
53028
53151
  init_exitCode();
@@ -53086,7 +53209,7 @@ async function assertRealPathInside(ctxDir, targetPath) {
53086
53209
  });
53087
53210
  }
53088
53211
  function resolveWorkspacePath(ctxDir, relPath) {
53089
- return relPath === "." ? ctxDir : join56(ctxDir, relPath);
53212
+ return relPath === "." ? ctxDir : join57(ctxDir, relPath);
53090
53213
  }
53091
53214
  function pathKind2(stats) {
53092
53215
  if (stats.isFile())
@@ -53241,7 +53364,7 @@ function assertWorkspaceWriteAllowed(relPath) {
53241
53364
  async function writeWorkspaceFile(input) {
53242
53365
  assertWorkspaceWriteAllowed(input.relPath);
53243
53366
  const destination = resolveWorkspacePath(input.ctxDir, input.relPath);
53244
- if (existsSync47(destination) && !input.overwrite) {
53367
+ if (existsSync48(destination) && !input.overwrite) {
53245
53368
  throw new ContextError(ExitCode.UserError, `workspace file already exists: ${input.relPath}`, {
53246
53369
  category: ErrorCategory.UserInputInvalid,
53247
53370
  path: input.relPath
@@ -54122,12 +54245,32 @@ function candidateRows(value) {
54122
54245
  churn: candidate.churn
54123
54246
  }));
54124
54247
  }
54248
+ function sourceReadingRows(value) {
54249
+ if (!isRecord24(value))
54250
+ return [];
54251
+ return asRecords(value.source_readings).map((reading) => ({
54252
+ ...typeof reading.source_id === "string" ? { source_id: reading.source_id } : {},
54253
+ ...typeof reading.segments_digest === "string" ? { segments_digest: reading.segments_digest } : {},
54254
+ ...typeof reading.coarse_read_digest === "string" ? { coarse_read_digest: reading.coarse_read_digest } : {},
54255
+ ...typeof reading.read_at === "string" ? { read_at: reading.read_at } : {},
54256
+ ...typeof reading.density_profile === "string" ? { density_profile: reading.density_profile } : {},
54257
+ reading_anchor_count: asRecords(reading.reading_anchors).length,
54258
+ section_proposal_count: asRecords(reading.section_proposals).length,
54259
+ reading_anchors: reading.reading_anchors,
54260
+ section_proposals: reading.section_proposals
54261
+ }));
54262
+ }
54125
54263
  function compactAlignLedger(record, value, options) {
54126
54264
  if (!isRecord24(value))
54127
54265
  return { candidates: [], block_dispositions: [], counts: { candidates: 0 } };
54128
54266
  const allCandidates = candidateRows(value);
54267
+ const allSourceReadings = sourceReadingRows(value);
54268
+ const sourceReadings = allSourceReadings.filter((reading) => options.sourceId === undefined || reading.source_id === options.sourceId);
54129
54269
  const filtered = allCandidates.filter((candidate) => (options.status === undefined || candidate.status === options.status) && (options.candidateId === undefined || candidate.candidate_id === options.candidateId));
54130
- const wantsDetail = options.status !== undefined || options.candidateId !== undefined || options.pageSize !== undefined || options.pageToken !== undefined;
54270
+ const shouldShowCandidates = options.sourceId === undefined || options.status !== undefined || options.candidateId !== undefined;
54271
+ const candidateRowsForDetail = shouldShowCandidates ? filtered : [];
54272
+ const sourceReadingsForDetail = options.sourceId === undefined ? [] : sourceReadings;
54273
+ const wantsDetail = options.sourceId !== undefined || options.status !== undefined || options.candidateId !== undefined || options.pageSize !== undefined || options.pageToken !== undefined;
54131
54274
  if (!wantsDetail) {
54132
54275
  const firstCandidateId = allCandidates.find((candidate) => candidate.candidate_id !== undefined)?.candidate_id;
54133
54276
  const firstStatus = allCandidates.find((candidate) => candidate.status !== undefined)?.status;
@@ -54136,10 +54279,18 @@ function compactAlignLedger(record, value, options) {
54136
54279
  last_batch_id: value.last_batch_id,
54137
54280
  summary: {
54138
54281
  candidates: allCandidates.length,
54282
+ source_readings: allSourceReadings.length,
54139
54283
  block_dispositions: asRecords(value.block_dispositions).length,
54140
54284
  by_status: countBy(allCandidates.map((candidate) => candidate.status ?? "unknown")),
54141
54285
  by_node_type: countBy(allCandidates.map((candidate) => candidate.node_type ?? "unknown"))
54142
54286
  },
54287
+ source_reading_index: allSourceReadings.slice(0, DEFAULT_INDEX_LIMIT).map((reading) => ({
54288
+ source_id: reading.source_id,
54289
+ density_profile: reading.density_profile,
54290
+ reading_anchor_count: reading.reading_anchor_count,
54291
+ section_proposal_count: reading.section_proposal_count,
54292
+ read_at: reading.read_at
54293
+ })),
54143
54294
  candidate_index: allCandidates.slice(0, DEFAULT_INDEX_LIMIT).map((candidate) => ({
54144
54295
  candidate_id: candidate.candidate_id,
54145
54296
  current_slug: candidate.current_slug,
@@ -54162,11 +54313,19 @@ function compactAlignLedger(record, value, options) {
54162
54313
  }]
54163
54314
  };
54164
54315
  }
54165
- const page = paginate(filtered, options);
54316
+ const page = paginate(candidateRowsForDetail, options);
54166
54317
  return {
54167
54318
  mode: "detail",
54168
54319
  last_batch_id: value.last_batch_id,
54169
54320
  filters: compactFilters(options),
54321
+ source_readings: sourceReadingsForDetail,
54322
+ ...options.sourceId === undefined ? { source_reading_index: allSourceReadings.slice(0, DEFAULT_INDEX_LIMIT).map((reading) => ({
54323
+ source_id: reading.source_id,
54324
+ density_profile: reading.density_profile,
54325
+ reading_anchor_count: reading.reading_anchor_count,
54326
+ section_proposal_count: reading.section_proposal_count,
54327
+ read_at: reading.read_at
54328
+ })) } : {},
54170
54329
  candidates: page.items,
54171
54330
  block_dispositions: asRecords(value.block_dispositions).map((item) => ({
54172
54331
  block_id: item.block_id,
@@ -54177,9 +54336,18 @@ function compactAlignLedger(record, value, options) {
54177
54336
  ...page.page !== undefined ? { page: page.page } : {},
54178
54337
  counts: {
54179
54338
  matched_candidates: filtered.length,
54339
+ candidate_filter_applied: shouldShowCandidates,
54180
54340
  shown_candidates: page.items.length,
54341
+ matched_source_readings: sourceReadings.length,
54342
+ shown_source_readings: sourceReadingsForDetail.length,
54181
54343
  block_dispositions: asRecords(value.block_dispositions).length
54182
- }
54344
+ },
54345
+ ...options.sourceId !== undefined && !shouldShowCandidates ? { agent_hints: [{
54346
+ code: "workflow-ledger-source-filter",
54347
+ severity: "info",
54348
+ message: "--source filters source_readings only; candidates do not carry source_id.",
54349
+ next_action: "Add --status or --candidate-id when you also need candidate rows from the ledger view."
54350
+ }] } : {}
54183
54351
  };
54184
54352
  }
54185
54353
  function compactAlignAggregate(record, value, options) {
@@ -55514,6 +55682,16 @@ function coverageSkipPatch(input) {
55514
55682
  }]
55515
55683
  };
55516
55684
  }
55685
+ function coverageSkipUnresolvedPatch(input) {
55686
+ return {
55687
+ schema_version: COVERAGE_DISPOSITION_SCHEMA_VERSION,
55688
+ dispositions: input.candidates.filter((candidate) => candidate.status === "unresolved").map((candidate) => ({
55689
+ candidate_id: candidate.candidate_id,
55690
+ action: "skip",
55691
+ reason: input.reason
55692
+ }))
55693
+ };
55694
+ }
55517
55695
  function renderCompileClosedFeedbackBody(input) {
55518
55696
  const lines = [];
55519
55697
  if (input.workflowSummary)
@@ -56191,11 +56369,10 @@ function candidateId(batchId, localRef, slug) {
56191
56369
  const hash2 = createHash8("sha256").update(`${batchId}\x00${localRef}\x00${slug}`).digest("hex").slice(0, 16);
56192
56370
  return `cand_${hash2}`;
56193
56371
  }
56194
- function cloneLedger(ledger, batchId) {
56195
- return ledger ? {
56196
- ...ledger,
56197
- last_batch_id: batchId,
56198
- id_mappings_from_last_batch: [],
56372
+ function cloneLedgerState(ledger) {
56373
+ return {
56374
+ schema_version: ledger.schema_version,
56375
+ source_readings: cloneSourceReadings(ledger.source_readings),
56199
56376
  candidates: ledger.candidates.map((candidate) => ({
56200
56377
  ...candidate,
56201
56378
  block_ids: [...candidate.block_ids],
@@ -56210,10 +56387,14 @@ function cloneLedger(ledger, batchId) {
56210
56387
  proposed_sections: ledger.proposed_sections.map((item) => ({ ...item })),
56211
56388
  ops_history: [...ledger.ops_history],
56212
56389
  warnings: [...ledger.warnings]
56213
- } : {
56390
+ };
56391
+ }
56392
+ function emptyLedger2(batchId) {
56393
+ return {
56214
56394
  schema_version: ALIGN_CANDIDATE_LEDGER_SCHEMA_VERSION,
56215
56395
  last_batch_id: batchId,
56216
56396
  id_mappings_from_last_batch: [],
56397
+ source_readings: [],
56217
56398
  candidates: [],
56218
56399
  block_dispositions: [],
56219
56400
  parent_hints: [],
@@ -56223,6 +56404,55 @@ function cloneLedger(ledger, batchId) {
56223
56404
  warnings: []
56224
56405
  };
56225
56406
  }
56407
+ function cloneLedger(ledger, batchId) {
56408
+ if (ledger === undefined)
56409
+ return emptyLedger2(batchId);
56410
+ return {
56411
+ ...cloneLedgerState(ledger),
56412
+ last_batch_id: batchId,
56413
+ id_mappings_from_last_batch: []
56414
+ };
56415
+ }
56416
+ function cloneSourceReadings(value) {
56417
+ return (value ?? []).map((reading) => ({
56418
+ ...reading,
56419
+ reading_anchors: reading.reading_anchors.map((anchor) => ({ ...anchor })),
56420
+ section_proposals: reading.section_proposals.map((section) => ({ ...section }))
56421
+ }));
56422
+ }
56423
+ function cloneLedgerForSourceReading(ledger) {
56424
+ if (ledger === undefined)
56425
+ return emptyLedger2("coarse-read");
56426
+ return {
56427
+ ...cloneLedgerState(ledger),
56428
+ last_batch_id: ledger.last_batch_id,
56429
+ id_mappings_from_last_batch: ledger.id_mappings_from_last_batch.map((mapping) => ({ ...mapping }))
56430
+ };
56431
+ }
56432
+ function upsertSourceReading(input) {
56433
+ const ledger = cloneLedgerForSourceReading(input.ledger);
56434
+ const densityProfile = text(input.coarseRead.density_profile);
56435
+ if (densityProfile === undefined) {
56436
+ throw new ContextError(ExitCode.UserError, "density_profile is required before storing source reading", {
56437
+ category: ErrorCategory.SchemaInvalid,
56438
+ path: "density_profile"
56439
+ });
56440
+ }
56441
+ const next = {
56442
+ source_id: input.sourceId,
56443
+ segments_digest: input.segmentsDigest,
56444
+ coarse_read_digest: input.coarseReadDigest,
56445
+ read_at: (input.now ?? new Date).toISOString(),
56446
+ density_profile: densityProfile,
56447
+ reading_anchors: Array.isArray(input.coarseRead.reading_anchors) ? input.coarseRead.reading_anchors.filter(isRecord26).map((anchor) => ({ ...anchor })) : [],
56448
+ section_proposals: Array.isArray(input.coarseRead.section_proposals) ? input.coarseRead.section_proposals.filter(isRecord26).map((section) => ({ ...section })) : []
56449
+ };
56450
+ ledger.source_readings = [
56451
+ ...(ledger.source_readings ?? []).filter((reading) => reading.source_id !== input.sourceId),
56452
+ next
56453
+ ].sort((left, right) => left.source_id.localeCompare(right.source_id));
56454
+ return ledger;
56455
+ }
56226
56456
  function terminal(candidate) {
56227
56457
  return candidate?.status === "rejected" || candidate?.status === "superseded";
56228
56458
  }
@@ -56754,6 +56984,65 @@ function nodesFromDecision(decision, sources, language) {
56754
56984
  };
56755
56985
  });
56756
56986
  }
56987
+ function nodeWithRecomputedSources(node2, sources) {
56988
+ const sourceIds = sourceIdsForNode(sources, node2.slug);
56989
+ return {
56990
+ slug: node2.slug,
56991
+ type: node2.type,
56992
+ tags: node2.tags,
56993
+ title: node2.title,
56994
+ sources: sourceIds.evidence,
56995
+ ...sourceIds.context.length > 0 ? { context_sources: sourceIds.context } : {},
56996
+ ...node2.summary !== undefined ? { summary: node2.summary } : {},
56997
+ ...node2.language !== undefined ? { language: node2.language } : {},
56998
+ ...node2.contains_parent !== undefined ? { contains_parent: node2.contains_parent } : {},
56999
+ ...node2.planned_sections !== undefined ? { planned_sections: node2.planned_sections } : {},
57000
+ ...node2.domain_gate !== undefined ? { domain_gate: node2.domain_gate } : {},
57001
+ ...node2.action_gate !== undefined ? { action_gate: node2.action_gate } : {}
57002
+ };
57003
+ }
57004
+ function mergeSources(input) {
57005
+ const currentBySource = new Map(input.current.map((source2) => [source2.source_id, source2]));
57006
+ const seen = new Set;
57007
+ const merged = input.previous.map((source2) => {
57008
+ const replacement = currentBySource.get(source2.source_id);
57009
+ if (replacement !== undefined) {
57010
+ seen.add(source2.source_id);
57011
+ return replacement;
57012
+ }
57013
+ return source2;
57014
+ });
57015
+ for (const source2 of input.current) {
57016
+ if (!seen.has(source2.source_id))
57017
+ merged.push(source2);
57018
+ }
57019
+ return merged;
57020
+ }
57021
+ function mergeNodes(input) {
57022
+ const currentBySlug = new Map(input.current.map((node2) => [node2.slug, node2]));
57023
+ const seen = new Set;
57024
+ const merged = input.previous.map((node2) => {
57025
+ const replacement = currentBySlug.get(node2.slug);
57026
+ seen.add(node2.slug);
57027
+ return nodeWithRecomputedSources(replacement ?? node2, input.sources);
57028
+ });
57029
+ for (const node2 of input.current) {
57030
+ if (!seen.has(node2.slug))
57031
+ merged.push(nodeWithRecomputedSources(node2, input.sources));
57032
+ }
57033
+ return merged;
57034
+ }
57035
+ function mergeEdges(input) {
57036
+ const byKey = new Map;
57037
+ const put = (edge2) => {
57038
+ if (!input.activeSlugs.has(edge2.from) || !input.activeSlugs.has(edge2.to))
57039
+ return;
57040
+ byKey.set(`${edge2.type}\x00${edge2.from}\x00${edge2.to}`, edge2);
57041
+ };
57042
+ input.previous.forEach(put);
57043
+ input.current.forEach(put);
57044
+ return [...byKey.values()].sort((left, right) => `${left.from}\x00${left.to}`.localeCompare(`${right.from}\x00${right.to}`));
57045
+ }
56757
57046
  function isCitationEligibleForNode(block, slug) {
56758
57047
  if (block.status === "owned")
56759
57048
  return block.owner === slug || block.coverable_by.includes(slug);
@@ -56769,6 +57058,16 @@ function isVisibleNonCitationForNode(block, slug) {
56769
57058
  function isContainerDomain(nodes, node2) {
56770
57059
  return node2.type === "domain" && nodes.some((candidate) => candidate.contains_parent === node2.slug);
56771
57060
  }
57061
+ function isExplicitNoWriteNode2(node2) {
57062
+ return node2.planned_sections !== undefined && node2.planned_sections.length === 0;
57063
+ }
57064
+ function hasFinalizedGraphLink2(ownership, node2) {
57065
+ if (typeof node2.contains_parent === "string" && node2.contains_parent.length > 0)
57066
+ return true;
57067
+ if ((ownership.nodes ?? []).some((candidate) => candidate.contains_parent === node2.slug))
57068
+ return true;
57069
+ return (ownership.edges ?? []).some((edge2) => edge2.from === node2.slug || edge2.to === node2.slug);
57070
+ }
56772
57071
  function auditFinalizedCitationEvidence(ownership) {
56773
57072
  const nodes = ownership.nodes ?? [];
56774
57073
  const blocks = ownership.sources.flatMap((source2) => source2.blocks);
@@ -56778,6 +57077,9 @@ function auditFinalizedCitationEvidence(ownership) {
56778
57077
  if (blocks.some((block) => isCitationEligibleForNode(block, node2.slug)))
56779
57078
  return [];
56780
57079
  const visibleBlocks = blocks.filter((block) => isVisibleNonCitationForNode(block, node2.slug)).map((block) => block.block_id).filter((blockId2) => typeof blockId2 === "string" && blockId2.length > 0);
57080
+ if (isExplicitNoWriteNode2(node2) && (visibleBlocks.length > 0 || hasFinalizedGraphLink2(ownership, node2))) {
57081
+ return [];
57082
+ }
56781
57083
  return [{
56782
57084
  kind: "node_without_citation_evidence",
56783
57085
  node_slug: node2.slug,
@@ -56835,6 +57137,31 @@ function buildSourceOwnershipFromFinalized(input) {
56835
57137
  summary: summary(sources)
56836
57138
  };
56837
57139
  }
57140
+ function mergeIncrementalSourceOwnership(input) {
57141
+ const sources = mergeSources({
57142
+ previous: input.previous.sources,
57143
+ current: input.current.sources
57144
+ });
57145
+ const nodes = mergeNodes({
57146
+ previous: input.previous.nodes ?? [],
57147
+ current: input.current.nodes ?? [],
57148
+ sources
57149
+ });
57150
+ const activeSlugs = new Set(nodes.map((node2) => node2.slug));
57151
+ const edges = mergeEdges({
57152
+ previous: input.previous.edges ?? [],
57153
+ current: input.current.edges ?? [],
57154
+ activeSlugs
57155
+ });
57156
+ return {
57157
+ schema_version: SOURCE_OWNERSHIP_SCHEMA_VERSION,
57158
+ generated_at: input.current.generated_at,
57159
+ nodes,
57160
+ ...edges.length > 0 ? { edges } : {},
57161
+ sources,
57162
+ summary: summary(sources)
57163
+ };
57164
+ }
56838
57165
 
56839
57166
  // src/workflow/alignConstraintRegistry.ts
56840
57167
  function duplicateValues2(values) {
@@ -56917,7 +57244,7 @@ function structureIssueHint(issue) {
56917
57244
  const availableNodeSlugs = sortedUnique(context.availableNodeSlugs ?? []);
56918
57245
  const availableBlockIds = sortedUnique(context.availableBlockIds ?? []);
56919
57246
  const gateHelp = path8.includes("action_gate") || path8.includes("domain_gate") ? " For gate fields, follow plugin/skills/skill-align-workflow/references/gates.md." : "";
56920
- const nextAction = context.reasonCode === "unknown-node-ref" ? `Use one of agent_hints[].available_node_refs for *_ref fields, then resubmit the corrected align-structure-decision.${gateHelp}` : context.correctShape !== undefined ? `Reshape this entry to match agent_hints[].correct_shape and resubmit the structure decision.${gateHelp}` : `Read \`context schema align-structure-decision\` and resubmit the structure decision.${gateHelp}`;
57247
+ const nextAction = context.reasonCode === "unknown-node-ref" ? `Use one of agent_hints[].available_node_refs for *_ref fields, then resubmit the corrected align-structure-decision.${gateHelp}` : context.reasonCode === "existing-node-type-drift" ? "Do not change node_type for an existing or previously removed slug. In incremental align, reference the existing slug from contains_parent, edges, domain_gate.child_refs, or block_ownership instead of redeclaring it. If the concept really needs a different type, use a new slug; if the old slug should be retired first, use `context drop` or an explicit structure correction path. `context align --scan --full` does not bypass this guard." : context.reasonCode === "section-owner-not-current-node" ? "sections[] plans Sections only for nodes declared in the current align-structure-decision. Move this Section plan to a current node or omit it for the previous finalized Node; previous Nodes can still be referenced from contains_parent, edges, domain_gate.child_refs, and block_ownership visibility." : context.correctShape !== undefined ? `Reshape this entry to match agent_hints[].correct_shape and resubmit the structure decision.${gateHelp}` : `Read \`context schema align-structure-decision\` and resubmit the structure decision.${gateHelp}`;
56921
57248
  return {
56922
57249
  code: "align-structure-decision-invalid",
56923
57250
  severity: "error",
@@ -57245,7 +57572,8 @@ var SCHEMAS = {
57245
57572
  "density_profile must be one of macro, meso, micro, or single_pass.",
57246
57573
  "content_signals keys must be temporal_density, actor_density, step_density, or directive_density.",
57247
57574
  "content_signals values must be high, med, or low. Use med, not medium.",
57248
- "content_signals describe text shape only; they are not action/type/tag hints."
57575
+ "content_signals describe text shape only; they are not action/type/tag hints.",
57576
+ "align-coarse-read is a per-source input and latest checkpoint. Durable multi-source recall lives in align-candidate-ledger.source_readings."
57249
57577
  ]
57250
57578
  },
57251
57579
  "align-candidate-ops": {
@@ -57346,6 +57674,15 @@ var SCHEMAS = {
57346
57674
  schema_version: "align.candidate-ledger.v1",
57347
57675
  last_batch_id: "discovery_batch_0001",
57348
57676
  id_mappings_from_last_batch: [{ local_candidate_ref: "local:region", candidate_id: "cand_0001" }],
57677
+ source_readings: [{
57678
+ source_id: "local:demo",
57679
+ segments_digest: "sha256:segments",
57680
+ coarse_read_digest: "sha256:coarse",
57681
+ read_at: "2026-05-12T00:00:00.000Z",
57682
+ density_profile: "single_pass",
57683
+ reading_anchors: [{ anchor_id: "a0001", evidence_blocks: ["b0001"] }],
57684
+ section_proposals: [{ section_id: "s0001", block_ids: ["b0001"], context_prefix: "Overview." }]
57685
+ }],
57349
57686
  candidates: [{
57350
57687
  candidate_id: "cand_0001",
57351
57688
  current_slug: "data-region",
@@ -57361,6 +57698,9 @@ var SCHEMAS = {
57361
57698
  block_dispositions: []
57362
57699
  },
57363
57700
  notes: [
57701
+ "source_readings stores per-source coarse-read notes so multiple coarse-read submissions do not rely on the latest align-coarse-read checkpoint.",
57702
+ "source_readings is discovery memory only; final structure still comes from align-structure-decision and finalized ownership.",
57703
+ "source_readings are sorted by source_id for deterministic output; they are not an execution-order log.",
57364
57704
  "Batch-local refs may appear only in id_mappings_from_last_batch and must not leak into later batches.",
57365
57705
  "Merged and superseded chains must remain acyclic."
57366
57706
  ]
@@ -57492,7 +57832,14 @@ var SCHEMAS = {
57492
57832
  evidence_blocks: ["b0002"],
57493
57833
  rationale: "Failover depends on the region scope."
57494
57834
  }],
57495
- block_ownership: BLOCK_OWNERSHIP_EXAMPLE,
57835
+ block_ownership_defaults: [{
57836
+ source_id: "local:regional-ops",
57837
+ ownership_role: "owned",
57838
+ owners: ["data-region"],
57839
+ visible_to: ["data-region", "regional-operations"],
57840
+ reason: "Remaining blocks in this source describe the data-region term."
57841
+ }],
57842
+ block_ownership: BLOCK_OWNERSHIP_EXAMPLE.filter((item) => item.block_id !== "b0002"),
57496
57843
  unresolved: [
57497
57844
  {
57498
57845
  question_id: "q_failover_owner",
@@ -57505,16 +57852,22 @@ var SCHEMAS = {
57505
57852
  "Use align-segments.generation_policy when choosing node.title, node.summary, rationale prose, and planned Section wording.",
57506
57853
  "nodes[].contains_parent is the only hierarchy field; edges[] only accepts depends_on.",
57507
57854
  "domain nodes need domain_gate; unresolved or weak domain gates are downgraded to entity.",
57855
+ "When align-segments.incremental.mode is incremental, finalize preserves previous finalized ownership for unscanned sources. The current decision should describe only the scanned evidence while referencing previous active Nodes when needed.",
57856
+ "In incremental mode, absence of an old Node or edge is not a deletion signal; use a full align, drop source, or explicit structure correction path for removals.",
57857
+ "Incremental finalize may reference previous active Nodes from nodes[].contains_parent, edges[].from/to, domain_gate.child_refs, and block_ownership owners/visible_to/primary_owner. Do not redeclare an old parent/domain just to attach new children.",
57858
+ "sections[].owner is intentionally limited to nodes declared in the current align-structure-decision payload; do not plan new Sections for previous finalized Nodes in an incremental payload.",
57859
+ "Existing and previously removed Node slugs cannot change node_type in either incremental or full finalize. context align --scan --full controls replacement/removal semantics, not type-drift bypass. Use a new slug for a different type, or retire the old slug through context drop or explicit structure correction first.",
57508
57860
  "action_probe is the only place for action qualification booleans; action_gate accepts inference_sources only.",
57509
57861
  "If the current candidate ledger already has action_probe for this action node and the digest matches, finalize may hydrate it. Directly new action nodes must provide action_probe explicitly.",
57510
57862
  "action_gate.inference_sources needs actor, outcome_or_goal, repeatability_or_plan, and answerability. source_type must be one of explicit-block / heading-and-block / ref-node / inferred-from-block. explicit-block and heading-and-block entries may omit rationale; ref-node and inferred-from-block mappings must explain rationale.",
57863
+ "For large finalize payloads, use block_ownership_defaults[] to assign all remaining coverable blocks from a source_id to one role/owner, then use block_ownership[] only for block-level exceptions. The CLI expands defaults before validation; explicit block_ownership[] entries override defaults for the same block_id.",
57511
57864
  "Each block_ownership[] entry sets `ownership_role` to one of owned / shared / context_only / ignored / unresolved.",
57512
57865
  "owned: exactly one slug in `owners[]`, plus `visible_to[]`. Do not set `primary_owner` or `context_prefix`. reason is optional for mechanical owned cases.",
57513
57866
  "shared: at least two slugs in `owners[]`, plus `primary_owner` chosen from those owners, plus `visible_to[]` and `reason`. The primary owner is the only Node allowed to author cited Sections from that block; secondary owners must request full text or raise an ownership challenge before citing it.",
57514
57867
  "context_only: omit `owners` and `primary_owner` entirely. Required: `context_prefix` (short summary travelling with citing Sections), `visible_to[]`. reason is optional for mechanical context-only cases. The block carries context but is not citation evidence on its own.",
57515
57868
  "ignored: omit `owners`, `primary_owner`, `context_prefix`, and `visible_to`. reason is optional for mechanical ignored cases. Use for outdated markers, navigation/external-link blocks, and placeholders without independent knowledge.",
57516
57869
  "unresolved: omit `owners`, `primary_owner`, and `context_prefix`. Required: `question_id` (matching a top-level `unresolved[].question_id`) and `reason`. Use when classification is blocked by missing evidence.",
57517
- "Every coverable block must have exactly one ownership disposition; the validator returns `agent_hints[].correct_shape` with the exact JSON skeleton when a role-specific field is wrong.",
57870
+ "Every coverable block must have exactly one ownership disposition after block_ownership_defaults[] expansion; the validator returns `agent_hints[].correct_shape` with the exact JSON skeleton when a role-specific field is wrong.",
57518
57871
  "block_ownership assigns evidence visibility and citation authority only; write Section kinds in sections[].section_kind."
57519
57872
  ]
57520
57873
  }
@@ -57527,6 +57880,92 @@ function alignWorkflowSchemaExample(name) {
57527
57880
  }
57528
57881
 
57529
57882
  // src/workflow/alignStructureOwnership.ts
57883
+ function expandOwnershipDefaults(input) {
57884
+ if (input.blockOwnershipDefaults === undefined)
57885
+ return input.blockOwnership;
57886
+ if (!Array.isArray(input.blockOwnership))
57887
+ return input.blockOwnership;
57888
+ if (!Array.isArray(input.blockOwnershipDefaults)) {
57889
+ throwStructureIssues([{
57890
+ path: "block_ownership_defaults",
57891
+ message: "block_ownership_defaults must be an array when present",
57892
+ context: {}
57893
+ }]);
57894
+ }
57895
+ if (input.coverableBlocks === undefined) {
57896
+ throwStructureIssues([{
57897
+ path: "block_ownership_defaults",
57898
+ message: "block_ownership_defaults requires align-segments source metadata",
57899
+ context: {}
57900
+ }]);
57901
+ }
57902
+ const sourceByBlock = new Map(input.coverableBlocks.map((block) => [block.block_id, block.source_id]));
57903
+ const blocksBySource = new Map;
57904
+ for (const blockId2 of input.coverableBlockIds) {
57905
+ const sourceId = sourceByBlock.get(blockId2);
57906
+ if (sourceId === undefined)
57907
+ continue;
57908
+ const blocks = blocksBySource.get(sourceId) ?? [];
57909
+ blocks.push(blockId2);
57910
+ blocksBySource.set(sourceId, blocks);
57911
+ }
57912
+ const explicitBlockIds = new Set(input.blockOwnership.filter(isRecord28).map((entry) => stringValue2(entry.block_id)).filter((blockId2) => blockId2 !== undefined));
57913
+ const issues = [];
57914
+ const defaultsBySource = new Set;
57915
+ const expanded = [];
57916
+ input.blockOwnershipDefaults.forEach((raw, index) => {
57917
+ if (!isRecord28(raw)) {
57918
+ issues.push({ path: `block_ownership_defaults[${index}]`, message: "ownership default must be an object", context: {} });
57919
+ return;
57920
+ }
57921
+ if (raw.block_id !== undefined) {
57922
+ issues.push({
57923
+ path: `block_ownership_defaults[${index}].block_id`,
57924
+ message: "block_ownership_defaults entries must omit block_id; the CLI expands them across source blocks",
57925
+ context: {}
57926
+ });
57927
+ return;
57928
+ }
57929
+ const sourceId = stringValue2(raw.source_id);
57930
+ if (sourceId === undefined) {
57931
+ issues.push({
57932
+ path: `block_ownership_defaults[${index}].source_id`,
57933
+ message: "source_id is required",
57934
+ context: { availableBlockIds: input.coverableBlockIds }
57935
+ });
57936
+ return;
57937
+ }
57938
+ if (defaultsBySource.has(sourceId)) {
57939
+ issues.push({
57940
+ path: `block_ownership_defaults[${index}].source_id`,
57941
+ message: `duplicate ownership default for source "${sourceId}"`,
57942
+ context: { reasonCode: "duplicate-source-ownership-default" }
57943
+ });
57944
+ return;
57945
+ }
57946
+ defaultsBySource.add(sourceId);
57947
+ const blockIds = blocksBySource.get(sourceId);
57948
+ if (blockIds === undefined || blockIds.length === 0) {
57949
+ issues.push({
57950
+ path: `block_ownership_defaults[${index}].source_id`,
57951
+ message: `source "${sourceId}" has no coverable blocks in this align-segments payload`,
57952
+ context: {
57953
+ reasonCode: "unknown-source-ownership-default",
57954
+ availableBlockIds: input.coverableBlockIds
57955
+ }
57956
+ });
57957
+ return;
57958
+ }
57959
+ for (const blockId2 of blockIds) {
57960
+ if (explicitBlockIds.has(blockId2))
57961
+ continue;
57962
+ expanded.push({ ...raw, block_id: blockId2 });
57963
+ }
57964
+ });
57965
+ if (issues.length > 0)
57966
+ throwStructureIssues(issues);
57967
+ return [...expanded, ...input.blockOwnership];
57968
+ }
57530
57969
  function normalizeOwnership(value, refs, slugs, coverableBlockIds) {
57531
57970
  if (!Array.isArray(value))
57532
57971
  throwStructureIssues([{
@@ -57770,22 +58209,47 @@ function setRef(map, ref, slug, path8) {
57770
58209
  }
57771
58210
  map.set(ref, slug);
57772
58211
  }
57773
- function refMap(nodes) {
58212
+ function refMap(nodes, previous) {
57774
58213
  const map = new Map;
57775
58214
  const slugs = new Set;
58215
+ const currentSlugs = new Set;
58216
+ for (const node2 of previous?.nodes ?? []) {
58217
+ slugs.add(node2.slug);
58218
+ setRef(map, node2.slug, node2.slug, "previous.nodes[].slug");
58219
+ }
57776
58220
  for (const node2 of nodes) {
57777
- if (slugs.has(node2.slug)) {
58221
+ if (currentSlugs.has(node2.slug)) {
57778
58222
  reject3("nodes[].slug", `duplicate node slug "${node2.slug}"`, {
57779
58223
  reasonCode: "duplicate-node-slug",
57780
- availableNodeSlugs: [...slugs]
58224
+ availableNodeSlugs: [...slugs, node2.slug]
57781
58225
  });
57782
58226
  }
58227
+ currentSlugs.add(node2.slug);
58228
+ if (slugs.has(node2.slug)) {
58229
+ const existingType = previous?.nodes?.find((candidate) => candidate.slug === node2.slug)?.type;
58230
+ if (existingType === undefined) {
58231
+ reject3("nodes[].slug", `duplicate node slug "${node2.slug}"`, {
58232
+ reasonCode: "duplicate-node-slug",
58233
+ availableNodeSlugs: [...slugs]
58234
+ });
58235
+ }
58236
+ }
57783
58237
  slugs.add(node2.slug);
57784
58238
  setRef(map, node2.slug, node2.slug, "nodes[].slug");
57785
58239
  setRef(map, node2.llm_slug_hint, node2.slug, "nodes[].llm_slug_hint");
57786
58240
  }
57787
58241
  return map;
57788
58242
  }
58243
+ function previousNodeTypes(previous) {
58244
+ const types4 = new Map;
58245
+ for (const node2 of previous?.removed_nodes ?? []) {
58246
+ types4.set(node2.slug, node2.type);
58247
+ }
58248
+ for (const node2 of previous?.nodes ?? []) {
58249
+ types4.set(node2.slug, node2.type);
58250
+ }
58251
+ return types4;
58252
+ }
57789
58253
  function resolveRef2(refs, value, path8) {
57790
58254
  if (value === null || value === undefined)
57791
58255
  return value;
@@ -57800,6 +58264,9 @@ function resolveRef2(refs, value, path8) {
57800
58264
  }
57801
58265
  return resolved;
57802
58266
  }
58267
+ function nodeRefsForSlugs(refs, slugs) {
58268
+ return [...refs.entries()].filter(([, slug]) => slugs.has(slug)).map(([ref]) => ref).sort();
58269
+ }
57803
58270
  function parseNodes(value) {
57804
58271
  if (!Array.isArray(value))
57805
58272
  reject3("nodes", "nodes must be an array");
@@ -57906,8 +58373,7 @@ function downgradeNode(node2, warningKind, reason) {
57906
58373
  }
57907
58374
  };
57908
58375
  }
57909
- function normalizeNodes(nodes, refs) {
57910
- const slugs = new Set(nodes.map((node2) => node2.slug));
58376
+ function normalizeNodes(nodes, refs, allSlugs, previousTypes) {
57911
58377
  const warnings = [];
57912
58378
  const normalized = nodes.map((node2, index) => {
57913
58379
  const parent = resolveRef2(refs, node2.contains_parent_ref ?? node2.contains_parent, `nodes[${index}].contains_parent_ref`);
@@ -57915,21 +58381,40 @@ function normalizeNodes(nodes, refs) {
57915
58381
  ...node2,
57916
58382
  ...parent !== undefined ? { contains_parent: parent } : {}
57917
58383
  };
57918
- if (next.contains_parent !== null && next.contains_parent !== undefined && !slugs.has(next.contains_parent)) {
58384
+ if (next.contains_parent !== null && next.contains_parent !== undefined && !allSlugs.has(next.contains_parent)) {
57919
58385
  reject3(`nodes[${index}].contains_parent`, "contains_parent must reference a finalized node", {
57920
58386
  reasonCode: "unknown-finalized-parent",
57921
58387
  availableNodeRefs: nodeRefsForHint(refs),
57922
- availableNodeSlugs: [...slugs]
58388
+ availableNodeSlugs: [...allSlugs]
57923
58389
  });
57924
58390
  }
57925
58391
  if (next.node_type === "domain") {
57926
58392
  const gate = next.domain_gate;
57927
- const childRefs = gate?.child_refs ?? [];
57928
- const validGate = gate !== undefined && Array.isArray(gate.scope_blocks) && Array.isArray(childRefs) && typeof gate.grouping_reason === "string" && childRefs.every((ref) => refs.has(ref) || slugs.has(ref));
58393
+ const rawChildRefs = Array.isArray(gate?.child_refs) ? gate.child_refs : undefined;
58394
+ const childRefs = rawChildRefs?.map((ref) => typeof ref === "string" ? ref.trim() : "") ?? [];
58395
+ const validGate = gate !== undefined && Array.isArray(gate.scope_blocks) && rawChildRefs !== undefined && typeof gate.grouping_reason === "string" && childRefs.length === rawChildRefs.length && childRefs.every((ref) => ref.length > 0 && (refs.has(ref) || allSlugs.has(ref)));
57929
58396
  if (!validGate) {
57930
58397
  const downgraded = downgradeNode(next, "domain_downgrade", "domain_gate is missing or references unresolved child refs");
57931
58398
  next = downgraded.node;
57932
58399
  warnings.push(downgraded.warning);
58400
+ } else {
58401
+ const canonicalChildRefs = childRefs.map((ref) => refs.get(ref) ?? ref);
58402
+ const duplicateChildRef = canonicalChildRefs.find((ref, refIndex) => canonicalChildRefs.indexOf(ref) !== refIndex);
58403
+ if (duplicateChildRef !== undefined) {
58404
+ reject3(`nodes[${index}].domain_gate.child_refs`, `domain_gate.child_refs contains duplicate child "${duplicateChildRef}"`, {
58405
+ reasonCode: "duplicate-domain-child-ref",
58406
+ failedRef: duplicateChildRef,
58407
+ availableNodeRefs: nodeRefsForHint(refs),
58408
+ availableNodeSlugs: [...allSlugs]
58409
+ });
58410
+ }
58411
+ next = {
58412
+ ...next,
58413
+ domain_gate: {
58414
+ ...gate,
58415
+ child_refs: canonicalChildRefs
58416
+ }
58417
+ };
57933
58418
  }
57934
58419
  }
57935
58420
  if (next.node_type === "action") {
@@ -57945,6 +58430,14 @@ function normalizeNodes(nodes, refs) {
57945
58430
  validateInferenceSources(gate, `nodes[${index}].action_gate`);
57946
58431
  }
57947
58432
  }
58433
+ const previousType = previousTypes.get(next.slug);
58434
+ if (previousType !== undefined && previousType !== next.node_type) {
58435
+ reject3(`nodes[${index}].node_type`, `existing node "${next.slug}" is already ${previousType}; align finalize cannot change it to ${next.node_type}`, {
58436
+ reasonCode: "existing-node-type-drift",
58437
+ availableNodeRefs: nodeRefsForHint(refs),
58438
+ availableNodeSlugs: [...allSlugs]
58439
+ });
58440
+ }
57948
58441
  const plannedSections = next.planned_sections ?? [];
57949
58442
  for (const issue of alignStructureNodeConstraintIssues({ node: next, nodeIndex: index })) {
57950
58443
  reject3(issue.path, issue.message, { reasonCode: issue.code });
@@ -57973,11 +58466,18 @@ function hydrateActionProbesFromLedger(nodes, input) {
57973
58466
  return actionProbe !== undefined ? { ...node2, action_probe: actionProbe } : node2;
57974
58467
  });
57975
58468
  }
57976
- function assertNoContainsCycles(nodes, refs) {
58469
+ function assertNoContainsCycles(nodes, refs, previous) {
57977
58470
  const parentBySlug = new Map;
58471
+ for (const node2 of previous?.nodes ?? []) {
58472
+ if (typeof node2.contains_parent === "string" && node2.contains_parent.length > 0) {
58473
+ parentBySlug.set(node2.slug, node2.contains_parent);
58474
+ }
58475
+ }
57978
58476
  nodes.forEach((node2) => {
57979
58477
  if (typeof node2.contains_parent === "string" && node2.contains_parent.length > 0) {
57980
58478
  parentBySlug.set(node2.slug, node2.contains_parent);
58479
+ } else {
58480
+ parentBySlug.delete(node2.slug);
57981
58481
  }
57982
58482
  });
57983
58483
  nodes.forEach((node2, index) => {
@@ -57989,7 +58489,7 @@ function assertNoContainsCycles(nodes, refs) {
57989
58489
  reasonCode: "contains-parent-cycle",
57990
58490
  failedRef: current,
57991
58491
  availableNodeRefs: nodeRefsForHint(refs),
57992
- availableNodeSlugs: nodes.map((candidate) => candidate.slug)
58492
+ availableNodeSlugs: nodeSlugsForHint(refs)
57993
58493
  });
57994
58494
  }
57995
58495
  seen.add(current);
@@ -57997,18 +58497,30 @@ function assertNoContainsCycles(nodes, refs) {
57997
58497
  }
57998
58498
  });
57999
58499
  }
58000
- function normalizeSections(value, refs, slugs) {
58500
+ function normalizeSections(value, refs, currentSlugs) {
58001
58501
  if (!Array.isArray(value))
58002
58502
  reject3("sections", "sections must be an array");
58503
+ const currentRefs = nodeRefsForSlugs(refs, currentSlugs);
58003
58504
  return value.map((raw, index) => {
58004
58505
  if (!isRecord28(raw))
58005
58506
  reject3(`sections[${index}]`, "section must be an object");
58006
- const owner = resolveNodeRef(refs, stringValue2(raw.owner), `sections[${index}].owner`);
58007
- if (!owner || !slugs.has(owner)) {
58008
- reject3(`sections[${index}].owner`, "owner must reference a finalized node", {
58009
- reasonCode: "unknown-section-owner",
58010
- availableNodeRefs: nodeRefsForHint(refs),
58011
- availableNodeSlugs: [...slugs]
58507
+ const rawOwner = stringValue2(raw.owner);
58508
+ if (rawOwner === undefined)
58509
+ reject3(`sections[${index}].owner`, `sections[${index}].owner is required`);
58510
+ const owner = refs.get(rawOwner);
58511
+ if (owner === undefined) {
58512
+ reject3(`sections[${index}].owner`, `sections[${index}].owner references unknown node "${rawOwner}"`, {
58513
+ reasonCode: "unknown-node-ref",
58514
+ failedRef: rawOwner,
58515
+ availableNodeRefs: currentRefs,
58516
+ availableNodeSlugs: [...currentSlugs]
58517
+ });
58518
+ }
58519
+ if (!owner || !currentSlugs.has(owner)) {
58520
+ reject3(`sections[${index}].owner`, "owner must reference a node declared in this align-structure-decision payload", {
58521
+ reasonCode: "section-owner-not-current-node",
58522
+ availableNodeRefs: currentRefs,
58523
+ availableNodeSlugs: [...currentSlugs]
58012
58524
  });
58013
58525
  }
58014
58526
  const rationale = stringValue2(raw.rationale);
@@ -58074,12 +58586,18 @@ function normalizeAlignStructureDecision(input) {
58074
58586
  });
58075
58587
  }
58076
58588
  const parsedNodes = hydrateActionProbesFromLedger(parseNodes(input.payload.nodes), input);
58077
- const refs = refMap(parsedNodes);
58078
- const normalizedNodes = normalizeNodes(parsedNodes, refs);
58079
- const slugs = new Set(normalizedNodes.nodes.map((node2) => node2.slug));
58080
- assertNoContainsCycles(normalizedNodes.nodes, refs);
58081
- const sections = normalizeSections(input.payload.sections, refs, slugs);
58082
- const edges = normalizeEdges(input.payload.edges, refs, slugs);
58589
+ const previousForRefs = input.allowExistingNodeRefs === true ? input.previousOwnership : undefined;
58590
+ const refs = refMap(parsedNodes, previousForRefs);
58591
+ const previousTypes = previousNodeTypes(input.previousOwnership);
58592
+ const currentSlugs = new Set(parsedNodes.map((node2) => node2.slug));
58593
+ const allSlugs = new Set([
58594
+ ...currentSlugs,
58595
+ ...input.allowExistingNodeRefs === true ? (input.previousOwnership?.nodes ?? []).map((node2) => node2.slug) : []
58596
+ ]);
58597
+ const normalizedNodes = normalizeNodes(parsedNodes, refs, allSlugs, previousTypes);
58598
+ assertNoContainsCycles(normalizedNodes.nodes, refs, input.previousOwnership);
58599
+ const sections = normalizeSections(input.payload.sections, refs, currentSlugs);
58600
+ const edges = normalizeEdges(input.payload.edges, refs, allSlugs);
58083
58601
  validateStructureEvidence({
58084
58602
  nodes: normalizedNodes.nodes,
58085
58603
  sections,
@@ -58087,7 +58605,13 @@ function normalizeAlignStructureDecision(input) {
58087
58605
  coverableBlockIds: input.coverableBlockIds,
58088
58606
  reject: reject3
58089
58607
  });
58090
- const blockOwnership = normalizeOwnership(input.payload.block_ownership, refs, slugs, input.coverableBlockIds);
58608
+ const ownershipInput = expandOwnershipDefaults({
58609
+ blockOwnership: input.payload.block_ownership,
58610
+ blockOwnershipDefaults: input.payload.block_ownership_defaults,
58611
+ coverableBlockIds: input.coverableBlockIds,
58612
+ ...input.coverableBlocks !== undefined ? { coverableBlocks: input.coverableBlocks } : {}
58613
+ });
58614
+ const blockOwnership = normalizeOwnership(ownershipInput, refs, allSlugs, input.coverableBlockIds);
58091
58615
  const unresolved = normalizeUnresolved(input.payload.unresolved);
58092
58616
  validateUnresolvedQuestions({ blockOwnership, unresolved, reject: reject3 });
58093
58617
  const artifactKind = stringValue2(input.payload.artifact_kind);
@@ -58225,18 +58749,27 @@ async function finalizeAlignStructureDecisionCommand(input) {
58225
58749
  digest: input.workflowState.input_digests["align-candidate-ledger"],
58226
58750
  workflowInputDigests: input.workflowState.input_digests
58227
58751
  })).value : undefined;
58752
+ const previousOwnership = await readCurrentSourceOwnership(input.ctxDir);
58753
+ const isIncrementalFinalize = segments.incremental.mode === "incremental";
58228
58754
  const rawDecision = await readStructuredInput2(finalizePath);
58755
+ const coverableBlockSet = new Set(segments.coverage.coverable_block_ids);
58229
58756
  const decision = normalizeAlignStructureDecision({
58230
58757
  payload: rawDecision,
58231
58758
  coverableBlockIds: segments.coverage.coverable_block_ids,
58232
- ...candidateLedger !== undefined ? { candidateLedger } : {}
58759
+ coverableBlocks: segments.sources.flatMap((source2) => source2.blocks.filter((block) => coverableBlockSet.has(block.block_id)).map((block) => ({ block_id: block.block_id, source_id: source2.source_id }))),
58760
+ ...candidateLedger !== undefined ? { candidateLedger } : {},
58761
+ ...previousOwnership !== null ? { previousOwnership } : {},
58762
+ allowExistingNodeRefs: isIncrementalFinalize
58233
58763
  });
58234
- const previousOwnership = await readCurrentSourceOwnership(input.ctxDir);
58235
- const ownership = buildSourceOwnershipFromFinalized({
58764
+ const currentOwnership = buildSourceOwnershipFromFinalized({
58236
58765
  segments,
58237
58766
  decision,
58238
58767
  now: new Date
58239
58768
  });
58769
+ const ownership = isIncrementalFinalize && previousOwnership !== null ? mergeIncrementalSourceOwnership({
58770
+ previous: previousOwnership,
58771
+ current: currentOwnership
58772
+ }) : currentOwnership;
58240
58773
  const citationIssues = alignFinalizeConstraintIssues({ ownership });
58241
58774
  if (citationIssues.length > 0) {
58242
58775
  throw new ContextError(ExitCode.UserError, `align finalize rejected ${citationIssues.length} Node(s) without citation-eligible owned/shared-primary evidence`, {
@@ -58277,7 +58810,8 @@ async function finalizeAlignStructureDecisionCommand(input) {
58277
58810
  const removedNodes = finalizedRemovedNodes({
58278
58811
  previous: previousOwnership,
58279
58812
  next: ownership,
58280
- workflowState
58813
+ workflowState,
58814
+ mode: segments.incremental.mode
58281
58815
  });
58282
58816
  const publishedOwnership = {
58283
58817
  ...ownership,
@@ -58364,6 +58898,8 @@ async function finalizeAlignStructureDecisionCommand(input) {
58364
58898
  function finalizedRemovedNodes(input) {
58365
58899
  const nextSlugs = new Set((input.next.nodes ?? []).map((node2) => node2.slug));
58366
58900
  const carried = (input.previous?.removed_nodes ?? []).filter((node2) => !nextSlugs.has(node2.slug));
58901
+ if (input.mode === "incremental")
58902
+ return carried.sort((left, right) => left.slug.localeCompare(right.slug));
58367
58903
  const carriedSlugs = new Set(carried.map((node2) => node2.slug));
58368
58904
  const newlyRemoved = (input.previous?.nodes ?? []).filter((node2) => !nextSlugs.has(node2.slug) && !carriedSlugs.has(node2.slug)).map((node2) => ({
58369
58905
  slug: node2.slug,
@@ -58910,6 +59446,25 @@ async function runAlignCoarseReadCommand(input) {
58910
59446
  value: coarseRead,
58911
59447
  format: "yaml"
58912
59448
  });
59449
+ const previousLedger = await readExistingLedger({
59450
+ ctxDir: input.ctxDir,
59451
+ workflowState: input.workflowState
59452
+ });
59453
+ const ledger = upsertSourceReading({
59454
+ ...previousLedger !== undefined ? { ledger: previousLedger } : {},
59455
+ sourceId: coarseRead.source_id,
59456
+ segmentsDigest,
59457
+ coarseReadDigest: payloadRecord.digest,
59458
+ coarseRead
59459
+ });
59460
+ const ledgerRecord = await writeWorkflowPayloadWithDigest({
59461
+ ctxDir: input.ctxDir,
59462
+ workflowId: input.workflowState.workflow_id,
59463
+ scopeId: workflowScope(input.workflowState),
59464
+ payload: "align-candidate-ledger",
59465
+ value: ledger,
59466
+ format: "json"
59467
+ });
58913
59468
  const workflowState = await advanceCurrentWorkflow({
58914
59469
  ctxDir: input.ctxDir,
58915
59470
  family: "align",
@@ -58917,17 +59472,37 @@ async function runAlignCoarseReadCommand(input) {
58917
59472
  scopeId: workflowScope(input.workflowState),
58918
59473
  inputDigests: {
58919
59474
  ...input.workflowState.input_digests,
58920
- "align-coarse-read": payloadRecord.digest
59475
+ "align-coarse-read": payloadRecord.digest,
59476
+ "align-candidate-ledger": ledgerRecord.digest
58921
59477
  }
58922
59478
  });
58923
59479
  const payloadReceipt = workflowPayloadReceipt(payloadRecord, {
58924
59480
  nextCommand: "context align --ops -"
58925
59481
  });
59482
+ const ledgerReceipt = workflowPayloadReceipt(ledgerRecord, {
59483
+ view: ["--view ledger"],
59484
+ nextCommand: "context align --ops -"
59485
+ });
59486
+ const sourceId = coarseRead.source_id;
59487
+ const sourceReadingShowCommand = workflowPayloadShowCommand(ledgerRecord, [
59488
+ `--view ledger --source ${shellQuote(sourceId)}`,
59489
+ "--unwrap",
59490
+ "--format json"
59491
+ ]);
58926
59492
  if (input.format === "json") {
58927
59493
  writeJson3({
58928
59494
  action: "coarse-read-saved",
58929
59495
  workflow: workflowMetadata(workflowState),
58930
59496
  payload: payloadReceipt,
59497
+ ledger: ledgerReceipt,
59498
+ source_reading: {
59499
+ source_id: sourceId,
59500
+ show_command: sourceReadingShowCommand
59501
+ },
59502
+ summary: {
59503
+ source_readings: (ledger.source_readings ?? []).length,
59504
+ source_id: sourceId
59505
+ },
58931
59506
  next: "produce candidate ops with context align --ops -"
58932
59507
  });
58933
59508
  return;
@@ -58940,7 +59515,9 @@ async function runAlignCoarseReadCommand(input) {
58940
59515
  body: [
58941
59516
  workflowSummaryLine(workflowState),
58942
59517
  `digest: ${payloadRecord.digest}`,
58943
- `show: ${workflowPayloadShowCommand(payloadRecord)}`
59518
+ `show: ${workflowPayloadShowCommand(payloadRecord)}`,
59519
+ `candidate ledger: ${ledgerReceipt.show_command}`,
59520
+ `source reading: ${sourceReadingShowCommand}`
58944
59521
  ],
58945
59522
  next: "produce candidate ops with context align --ops -"
58946
59523
  }));
@@ -59341,9 +59918,11 @@ function collectRequestedBlock(value, previous) {
59341
59918
  function ignoredSourceIds(value) {
59342
59919
  return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.length > 0) : [];
59343
59920
  }
59344
- function coverageSkipMissingOptionsError(missingFields) {
59921
+ function coverageSkipMissingOptionsError(missingFields, mode = "single") {
59922
+ const flag = mode === "bulk" ? "--coverage-skip-unresolved" : "--coverage-skip";
59923
+ const command = mode === "bulk" ? 'context compile --coverage-skip-unresolved --coverage-disposition-node <slug> --payload-digest <digest> --reason "<reason>"' : 'context compile --coverage-skip <candidate-id> --coverage-disposition-node <slug> --payload-digest <digest> --reason "<reason>"';
59345
59924
  const reasonOnly = missingFields.length === 1 && missingFields[0] === "--reason <text>";
59346
- const message = reasonOnly ? "--coverage-skip requires --reason <text>" : `--coverage-skip is missing required option(s): ${missingFields.join(", ")}`;
59925
+ const message = reasonOnly ? `${flag} requires --reason <text>` : `${flag} is missing required option(s): ${missingFields.join(", ")}`;
59347
59926
  return new ContextError(ExitCode.UserError, message, {
59348
59927
  category: ErrorCategory.UserInputInvalid,
59349
59928
  missing_fields: missingFields,
@@ -59351,8 +59930,8 @@ function coverageSkipMissingOptionsError(missingFields) {
59351
59930
  code: reasonOnly ? "coverage-skip-reason-required" : "coverage-skip-required-options",
59352
59931
  severity: "error",
59353
59932
  message: reasonOnly ? "Skipping coverage requires a reason for auditability." : "Coverage skip needs every non-inferable option in one retry.",
59354
- next_action: "Retry with the candidate id, node, payload digest, and audit reason in one command.",
59355
- command: 'context compile --coverage-skip <candidate-id> --coverage-disposition-node <slug> --payload-digest <digest> --reason "<reason>"',
59933
+ next_action: mode === "single" ? "Retry with the candidate id, node, payload digest, and audit reason in one command." : "Retry with the node, payload digest, and audit reason in one command.",
59934
+ command,
59356
59935
  diagnostics: { missing_fields: missingFields }
59357
59936
  }]
59358
59937
  });
@@ -59362,7 +59941,7 @@ function compileStageForOptions(options) {
59362
59941
  return "node_draft_ready";
59363
59942
  if (typeof options.context === "string")
59364
59943
  return "node_context_ready";
59365
- if (typeof options.coverageDisposition === "string" || typeof options.coverageSkip === "string")
59944
+ if (typeof options.coverageDisposition === "string" || typeof options.coverageSkip === "string" || options.coverageSkipUnresolved === true)
59366
59945
  return null;
59367
59946
  if (typeof options.draft === "string")
59368
59947
  return "node_draft_ready";
@@ -59407,7 +59986,7 @@ async function writeCompileDraftChallengePayloads2(input) {
59407
59986
  function registerWorkflowCommands(program2) {
59408
59987
  registerWorkflowStateCommands(program2);
59409
59988
  registerAlignWorkflowCommand(program2);
59410
- program2.command("compile").description("Workflow helpers for /context:compile").option("--scan-changes", "print incremental compile workset without creating a workflow lock").addOption(new Option("--scan", "removed; use --scan-changes").hideHelp()).option("--format <format>", "with --scan-changes/--context/--source-refs, output format: json | table", "table").option("--view <view>", "compact output view: full | summary | source-refs | issues").option("--ignore-source <source-id>", "with --scan-changes, --context, or --source-refs, ignore refresh-applied source ids", collectIgnoredSource, []).option("--context <slug>", "prepare NodeContext payload for one align node and print summary/digests").option("--source-refs <slug>", "print compact citation source_ref list for one align node").option("--request-full-text <block-id>", "with --context, expand one secondary shared finalized ownership block", collectRequestedBlock, []).option("--node-cycle <slug>", "validate one node draft, prepare reconcile, review safe defaults, and apply in one command").option("--draft <slug>", "validate a compile draft JSON/YAML document from stdin for one node").option("--prepare", "with --draft --plan, also prepare the semantic reconcile payload").option("--draft-status <slug>", "print the current compile draft session summary").option("--draft-patch <slug>", "apply a compile draft patch for one node").option("--input <stdin>", "compile draft JSON/YAML document used with --draft; use '-' for stdin").option("--accept-safe-defaults", "with --node-cycle, accept mechanically safe defaults and stop if anything needs judgment").option("--coverage-disposition <stdin>", "coverage disposition patch JSON/YAML document; use '-' for stdin").option("--coverage-skip <candidate-id>", "skip one coverage candidate without writing a disposition file").option("--coverage-disposition-node <slug>", "node slug for --coverage-disposition").option("--coverage-disposition-replace", "replace existing dispositions for the patched coverage candidates").option("--payload-digest <digest>", "expected workflow payload digest for write commands").option("--reason <text>", "reason for --coverage-skip").option("--plan", "validate the draft without writing knowledge").option("--close", "run compile close: compact, rebuild index, append changelog, verify").option("--save-input", "with --draft, save the consumed draft under output/compile/compile.<slug>.draft.yaml").action(async (options) => {
59989
+ program2.command("compile").description("Workflow helpers for /context:compile").option("--scan-changes", "print incremental compile workset without creating a workflow lock").addOption(new Option("--scan", "removed; use --scan-changes").hideHelp()).option("--format <format>", "with --scan-changes/--context/--source-refs, output format: json | table", "table").option("--view <view>", "compact output view: full | summary | source-refs | issues").option("--ignore-source <source-id>", "with --scan-changes, --context, or --source-refs, ignore refresh-applied source ids", collectIgnoredSource, []).option("--context <slug>", "prepare NodeContext payload for one align node and print summary/digests").option("--source-refs <slug>", "print compact citation source_ref list for one align node").option("--request-full-text <block-id>", "with --context, expand one secondary shared finalized ownership block", collectRequestedBlock, []).option("--node-cycle <slug>", "validate one node draft, prepare reconcile, review safe defaults, and apply in one command").option("--draft <slug>", "validate a compile draft JSON/YAML document from stdin for one node").option("--prepare", "with --draft --plan, also prepare the semantic reconcile payload").option("--draft-status <slug>", "print the current compile draft session summary").option("--draft-patch <slug>", "apply a compile draft patch for one node").option("--input <stdin>", "compile draft JSON/YAML document used with --draft; use '-' for stdin").option("--accept-safe-defaults", "with --node-cycle, accept mechanically safe defaults and stop if anything needs judgment").option("--coverage-disposition <stdin>", "coverage disposition patch JSON/YAML document; use '-' for stdin").option("--coverage-skip <candidate-id>", "skip one coverage candidate without writing a disposition file").option("--coverage-skip-unresolved", "skip every unresolved coverage candidate in the node payload").option("--coverage-disposition-node <slug>", "node slug for --coverage-disposition").option("--coverage-disposition-replace", "replace existing dispositions for the patched coverage candidates").option("--payload-digest <digest>", "expected workflow payload digest for write commands").option("--reason <text>", "reason for coverage skip shortcuts").option("--plan", "validate the draft without writing knowledge").option("--close", "run compile close: compact, rebuild index, append changelog, verify").option("--save-input", "with --draft, save the consumed draft under output/compile/compile.<slug>.draft.yaml").action(async (options) => {
59411
59990
  if (options.scan === true) {
59412
59991
  throw new ContextError(ExitCode.UserError, "context compile --scan was removed; use context compile --scan-changes", {
59413
59992
  category: ErrorCategory.UserInputInvalid,
@@ -59421,9 +60000,9 @@ function registerWorkflowCommands(program2) {
59421
60000
  });
59422
60001
  }
59423
60002
  const wantsChanges = options.scanChanges === true;
59424
- const actionCount = countActions(wantsChanges, typeof options.context === "string", typeof options.sourceRefs === "string", typeof options.nodeCycle === "string", typeof options.draft === "string", typeof options.draftStatus === "string", typeof options.draftPatch === "string", typeof options.coverageDisposition === "string", typeof options.coverageSkip === "string", options.close === true);
60003
+ const actionCount = countActions(wantsChanges, typeof options.context === "string", typeof options.sourceRefs === "string", typeof options.nodeCycle === "string", typeof options.draft === "string", typeof options.draftStatus === "string", typeof options.draftPatch === "string", typeof options.coverageDisposition === "string", typeof options.coverageSkip === "string", options.coverageSkipUnresolved === true, options.close === true);
59425
60004
  if (actionCount !== 1) {
59426
- throw new ContextError(ExitCode.UserError, "usage: context compile --scan-changes [--format json|table] [--ignore-source <source-id>...] | --context <slug> [--request-full-text <block-id>...] [--ignore-source <source-id>...] | --source-refs <slug> [--format json|table] | --node-cycle <slug> --input - --accept-safe-defaults [--format json|table] | --draft <slug> --input - --plan [--prepare] | --draft-status <slug> [--format json|table] | --draft-patch <slug> --input - --payload-digest <digest> --plan | --coverage-disposition - --coverage-disposition-node <slug> --payload-digest <digest> [--coverage-disposition-replace] | --coverage-skip <candidate-id> --coverage-disposition-node <slug> --payload-digest <digest> --reason <text> | --close", { category: ErrorCategory.UserInputInvalid });
60005
+ throw new ContextError(ExitCode.UserError, "usage: context compile --scan-changes [--format json|table] [--ignore-source <source-id>...] | --context <slug> [--request-full-text <block-id>...] [--ignore-source <source-id>...] | --source-refs <slug> [--format json|table] | --node-cycle <slug> --input - --accept-safe-defaults [--format json|table] | --draft <slug> --input - --plan [--prepare] | --draft-status <slug> [--format json|table] | --draft-patch <slug> --input - --payload-digest <digest> --plan | --coverage-disposition - --coverage-disposition-node <slug> --payload-digest <digest> [--coverage-disposition-replace] | --coverage-skip <candidate-id> --coverage-disposition-node <slug> --payload-digest <digest> --reason <text> | --coverage-skip-unresolved --coverage-disposition-node <slug> --payload-digest <digest> --reason <text> | --close", { category: ErrorCategory.UserInputInvalid });
59427
60006
  }
59428
60007
  const viewWasExplicit = typeof options.view === "string";
59429
60008
  const view = compileOutputView(options.view);
@@ -59670,20 +60249,22 @@ function registerWorkflowCommands(program2) {
59670
60249
  });
59671
60250
  return;
59672
60251
  }
59673
- if (typeof options.coverageDisposition === "string" || typeof options.coverageSkip === "string") {
60252
+ if (typeof options.coverageDisposition === "string" || typeof options.coverageSkip === "string" || options.coverageSkipUnresolved === true) {
59674
60253
  const currentRead = await readCurrentWorkflow(ctx.ctxDir);
59675
- if (typeof options.coverageSkip === "string" && currentRead.status !== "ready") {
60254
+ const coverageSkipMode = options.coverageSkipUnresolved === true ? "bulk" : "single";
60255
+ const wantsCoverageSkip = typeof options.coverageSkip === "string" || options.coverageSkipUnresolved === true;
60256
+ if (wantsCoverageSkip && currentRead.status !== "ready") {
59676
60257
  const missingFields = [
59677
60258
  ...typeof options.coverageDispositionNode === "string" ? [] : ["--coverage-disposition-node <slug>"],
59678
60259
  ...typeof options.payloadDigest === "string" ? [] : ["--payload-digest <digest>"],
59679
60260
  ...typeof options.reason === "string" && options.reason.trim().length > 0 ? [] : ["--reason <text>"]
59680
60261
  ];
59681
60262
  if (missingFields.length > 0)
59682
- throw coverageSkipMissingOptionsError(missingFields);
60263
+ throw coverageSkipMissingOptionsError(missingFields, coverageSkipMode);
59683
60264
  }
59684
60265
  const current = requireCurrentWorkflow(currentRead);
59685
- if (typeof options.coverageSkip === "string" && (typeof options.reason !== "string" || options.reason.trim().length === 0)) {
59686
- throw coverageSkipMissingOptionsError(["--reason <text>"]);
60266
+ if (wantsCoverageSkip && (typeof options.reason !== "string" || options.reason.trim().length === 0)) {
60267
+ throw coverageSkipMissingOptionsError(["--reason <text>"], coverageSkipMode);
59687
60268
  }
59688
60269
  const requestedNode = typeof options.coverageDispositionNode === "string" ? options.coverageDispositionNode : undefined;
59689
60270
  const requestedDigest = typeof options.payloadDigest === "string" ? options.payloadDigest : undefined;
@@ -59703,8 +60284,9 @@ function registerWorkflowCommands(program2) {
59703
60284
  inferred.push(`payload digest: ${payload.digest}`);
59704
60285
  const value = payload.value;
59705
60286
  const candidates = Array.isArray(value.candidates) ? value.candidates : [];
59706
- const patchInput = typeof options.coverageSkip === "string" ? coverageSkipPatch({ candidateId: options.coverageSkip, reason: String(options.reason).trim() }) : await readStructuredInput2(resolveStdinOnlyInput(options.coverageDisposition, "--coverage-disposition"));
60287
+ const patchInput = typeof options.coverageSkip === "string" ? coverageSkipPatch({ candidateId: options.coverageSkip, reason: String(options.reason).trim() }) : options.coverageSkipUnresolved === true ? coverageSkipUnresolvedPatch({ candidates, reason: String(options.reason).trim() }) : await readStructuredInput2(resolveStdinOnlyInput(options.coverageDisposition, "--coverage-disposition"));
59707
60288
  const patch = await validateCoverageDispositions({ ctxDir: ctx.ctxDir, candidates, patch: patchInput });
60289
+ const skippedCount = patch.dispositions.filter((disposition2) => disposition2.action === "skip").length;
59708
60290
  const status = await applyCoverageDispositionPatch({
59709
60291
  ctxDir: ctx.ctxDir,
59710
60292
  candidates,
@@ -59713,8 +60295,8 @@ function registerWorkflowCommands(program2) {
59713
60295
  });
59714
60296
  process.stdout.write(formatFeedback({
59715
60297
  symbol: status.high_signal_unresolved > 0 ? "⚠" : "✓",
59716
- action: typeof options.coverageSkip === "string" ? "skipped" : "applied",
59717
- subject: typeof options.coverageSkip === "string" ? `coverage candidate ${options.coverageSkip}` : `coverage disposition ${options.coverageDispositionNode}`,
60298
+ action: wantsCoverageSkip ? "skipped" : "applied",
60299
+ subject: typeof options.coverageSkip === "string" ? `coverage candidate ${options.coverageSkip}` : options.coverageSkipUnresolved === true ? `${skippedCount} unresolved coverage candidate(s) for ${value.node ?? scopeId}` : `coverage disposition ${options.coverageDispositionNode}`,
59718
60300
  headline: formatCoverageSummary(status),
59719
60301
  body: [
59720
60302
  workflowSummaryLine(workflowState),
@@ -59903,7 +60485,7 @@ function registerWorkflowCommands(program2) {
59903
60485
  init_cliFeedback();
59904
60486
  init_errors();
59905
60487
  import { lstat as lstat3, readdir as readdir23 } from "node:fs/promises";
59906
- import { isAbsolute as isAbsolute7, join as join57, relative as relative13, resolve as resolve11 } from "node:path";
60488
+ import { isAbsolute as isAbsolute7, join as join58, relative as relative13, resolve as resolve11 } from "node:path";
59907
60489
 
59908
60490
  // src/lib/pathFreeCommandMatrix.ts
59909
60491
  var COMMAND_MATRIX = [
@@ -60180,12 +60762,12 @@ async function debugSourceRawPath(ctx, options) {
60180
60762
  function workspaceStorageRoots(ctxDir) {
60181
60763
  return {
60182
60764
  ctxDir,
60183
- raw: join57(ctxDir, "raw"),
60184
- knowledge: join57(ctxDir, "knowledge"),
60185
- output: join57(ctxDir, "output"),
60186
- archive: join57(ctxDir, "archive"),
60187
- decisions: join57(ctxDir, "decisions"),
60188
- cache: join57(ctxDir, ".cache")
60765
+ raw: join58(ctxDir, "raw"),
60766
+ knowledge: join58(ctxDir, "knowledge"),
60767
+ output: join58(ctxDir, "output"),
60768
+ archive: join58(ctxDir, "archive"),
60769
+ decisions: join58(ctxDir, "decisions"),
60770
+ cache: join58(ctxDir, ".cache")
60189
60771
  };
60190
60772
  }
60191
60773
  async function debugWorkspaceLocate(ctx, options) {
@@ -60275,7 +60857,7 @@ async function knowledgeNodeTypeDirs(ctxDir) {
60275
60857
  async function inspectNodeObject(ctxDir, slug) {
60276
60858
  const typeDirs = await knowledgeNodeTypeDirs(ctxDir);
60277
60859
  const candidates = await Promise.all(typeDirs.map((type) => {
60278
- const relPath = join57("knowledge", type, `${slug}.md`);
60860
+ const relPath = join58("knowledge", type, `${slug}.md`);
60279
60861
  return inspectWorkspacePath(ctxDir, relPath, relPath);
60280
60862
  }));
60281
60863
  return {
@@ -60291,7 +60873,7 @@ function safeArchiveSourceSegment(value) {
60291
60873
  async function inspectArchiveObject(ctx, archiveId) {
60292
60874
  const ctxDir = requireContext2(ctx, "debug storage inspect");
60293
60875
  const source2 = (ctx.sources?.sources ?? []).find((entry) => entry.id === archiveId);
60294
- const directRel = join57("archive", "sources", safeArchiveSourceSegment(archiveId));
60876
+ const directRel = join58("archive", "sources", safeArchiveSourceSegment(archiveId));
60295
60877
  const paths = [
60296
60878
  ...source2?.archive_path !== undefined ? [await inspectWorkspacePath(ctxDir, source2.archive_path, "source.archive_path")] : [],
60297
60879
  await inspectWorkspacePath(ctxDir, directRel, directRel)
@@ -60339,7 +60921,7 @@ async function walkForFileName(root, fileName, out2 = []) {
60339
60921
  throw error;
60340
60922
  }
60341
60923
  for (const entry of entries) {
60342
- const child = join57(root, entry.name);
60924
+ const child = join58(root, entry.name);
60343
60925
  if (entry.isDirectory()) {
60344
60926
  await walkForFileName(child, fileName, out2);
60345
60927
  } else if (entry.isFile() && entry.name === fileName) {
@@ -60349,7 +60931,7 @@ async function walkForFileName(root, fileName, out2 = []) {
60349
60931
  return out2;
60350
60932
  }
60351
60933
  async function inspectReviewObject(ctxDir, reviewId) {
60352
- const matches = await walkForFileName(join57(ctxDir, "output", "workflows"), `review.${reviewId}.yaml`);
60934
+ const matches = await walkForFileName(join58(ctxDir, "output", "workflows"), `review.${reviewId}.yaml`);
60353
60935
  return {
60354
60936
  object: `review:${reviewId}`,
60355
60937
  kind: "review",
@@ -60359,14 +60941,14 @@ async function inspectReviewObject(ctxDir, reviewId) {
60359
60941
  }
60360
60942
  async function inspectWorkflowObject(ctxDir, workflowId) {
60361
60943
  const current = await readCurrentWorkflow(ctxDir);
60362
- const workflowRoot = workspacePath(ctxDir, join57("output", "workflows", safePathSegment2(workflowId)), "workflow output root");
60944
+ const workflowRoot = workspacePath(ctxDir, join58("output", "workflows", safePathSegment2(workflowId)), "workflow output root");
60363
60945
  const currentMatches = current.status === "ready" && current.state.workflow_id === workflowId;
60364
60946
  return {
60365
60947
  object: `workflow:${workflowId}`,
60366
60948
  kind: "workflow",
60367
60949
  paths: [
60368
60950
  await inspectPath(workflowRoot, "workflow_output_root"),
60369
- ...currentMatches ? [await inspectWorkspacePath(ctxDir, join57(".cache", "workflows", "current.yaml"), "current_workflow_state")] : []
60951
+ ...currentMatches ? [await inspectWorkspacePath(ctxDir, join58(".cache", "workflows", "current.yaml"), "current_workflow_state")] : []
60370
60952
  ],
60371
60953
  current_workflow_state: current.status === "ready" ? {
60372
60954
  status: currentMatches ? "matches-query" : "latest-singleton-different-workflow",
@@ -60513,10 +61095,10 @@ async function buildDebugSnapshot(ctx) {
60513
61095
  manifest: cache.manifest
60514
61096
  },
60515
61097
  counts: {
60516
- raw_entries: await countDirectEntries(join57(ctxDir, "raw")),
60517
- knowledge_entries: await countDirectEntries(join57(ctxDir, "knowledge")),
60518
- output_entries: await countDirectEntries(join57(ctxDir, "output")),
60519
- archive_entries: await countDirectEntries(join57(ctxDir, "archive"))
61098
+ raw_entries: await countDirectEntries(join58(ctxDir, "raw")),
61099
+ knowledge_entries: await countDirectEntries(join58(ctxDir, "knowledge")),
61100
+ output_entries: await countDirectEntries(join58(ctxDir, "output")),
61101
+ archive_entries: await countDirectEntries(join58(ctxDir, "archive"))
60520
61102
  }
60521
61103
  };
60522
61104
  }
@@ -60590,11 +61172,11 @@ init_errors();
60590
61172
  init_cliFeedback();
60591
61173
  init_config();
60592
61174
  init_errors();
60593
- import { join as join61 } from "node:path";
61175
+ import { join as join62 } from "node:path";
60594
61176
 
60595
61177
  // src/build/llms.ts
60596
61178
  import { writeFile as writeFile24 } from "node:fs/promises";
60597
- import { join as join59 } from "node:path";
61179
+ import { join as join60 } from "node:path";
60598
61180
 
60599
61181
  // src/build/renderKnowledge.ts
60600
61182
  init_nodeRenderer();
@@ -60677,7 +61259,7 @@ init_nodeRenderer();
60677
61259
  init_exitCode();
60678
61260
  init_knowledge();
60679
61261
  import { mkdir as mkdir31 } from "node:fs/promises";
60680
- import { join as join58 } from "node:path";
61262
+ import { join as join59 } from "node:path";
60681
61263
  var MAX_TIMESTAMP_COLLISION_ATTEMPTS = 60;
60682
61264
  var SUMMARY_MAX_LENGTH = 160;
60683
61265
  var NODE_TYPE_ORDER = [NodeType2.domain, NodeType2.entity, NodeType2.action];
@@ -60704,11 +61286,11 @@ function isFsCode(error, code) {
60704
61286
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
60705
61287
  }
60706
61288
  async function createTimestampedPackageDir(outputRoot, packageName, now) {
60707
- const packageRoot = join58(outputRoot, packageName);
61289
+ const packageRoot = join59(outputRoot, packageName);
60708
61290
  await mkdir31(packageRoot, { recursive: true });
60709
61291
  for (let offsetSeconds = 0;offsetSeconds < MAX_TIMESTAMP_COLLISION_ATTEMPTS; offsetSeconds += 1) {
60710
61292
  const candidateTime = new Date(now.getTime() + offsetSeconds * 1000);
60711
- const candidate = join58(packageRoot, formatBuildTimestamp(candidateTime));
61293
+ const candidate = join59(packageRoot, formatBuildTimestamp(candidateTime));
60712
61294
  try {
60713
61295
  await mkdir31(candidate);
60714
61296
  return candidate;
@@ -60784,10 +61366,10 @@ function renderLlmsIndex(input) {
60784
61366
  }
60785
61367
  async function writeLlmsPackage(input) {
60786
61368
  const packageDir = await createTimestampedPackageDir(input.outputRoot, "llms-pkg", input.now);
60787
- await writeFile24(join59(packageDir, "llms.txt"), renderLlmsIndex(input), "utf8");
61369
+ await writeFile24(join60(packageDir, "llms.txt"), renderLlmsIndex(input), "utf8");
60788
61370
  await Promise.all(input.nodes.map(async (node2) => {
60789
61371
  const parsed = node2.located.parsed.node;
60790
- await writeFile24(join59(packageDir, buildNodeFileName(parsed)), renderExportNodeMarkdown({ node: parsed, sections: node2.activeSections }), "utf8");
61372
+ await writeFile24(join60(packageDir, buildNodeFileName(parsed)), renderExportNodeMarkdown({ node: parsed, sections: node2.activeSections }), "utf8");
60791
61373
  }));
60792
61374
  return {
60793
61375
  packageDir,
@@ -60797,7 +61379,7 @@ async function writeLlmsPackage(input) {
60797
61379
 
60798
61380
  // src/build/skillsPack.ts
60799
61381
  import { mkdir as mkdir32, writeFile as writeFile25 } from "node:fs/promises";
60800
- import { join as join60 } from "node:path";
61382
+ import { join as join61 } from "node:path";
60801
61383
  var KNOWLEDGE_QUERY_PROCEDURE = [
60802
61384
  "1. For a named topic, read the matching wiki file directly from `wikis/`.",
60803
61385
  "2. For a broad question, grep `wikis/` with relevant keywords, then read the matching wiki files.",
@@ -60858,23 +61440,23 @@ function renderKnowledgeQuerySkill() {
60858
61440
  }
60859
61441
  async function writeSkillsPack(input) {
60860
61442
  const packageDir = await createTimestampedPackageDir(input.outputRoot, "skills-pkg", input.now);
60861
- const guidesDir = join60(packageDir, "guides");
60862
- const skillsDir = join60(packageDir, "skills");
60863
- const wikisDir = join60(packageDir, "wikis");
61443
+ const guidesDir = join61(packageDir, "guides");
61444
+ const skillsDir = join61(packageDir, "skills");
61445
+ const wikisDir = join61(packageDir, "wikis");
60864
61446
  await Promise.all([
60865
61447
  mkdir32(guidesDir, { recursive: true }),
60866
61448
  mkdir32(skillsDir, { recursive: true }),
60867
61449
  mkdir32(wikisDir, { recursive: true }),
60868
- mkdir32(join60(packageDir, "rules"), { recursive: true }),
60869
- mkdir32(join60(packageDir, "integrations"), { recursive: true })
61450
+ mkdir32(join61(packageDir, "rules"), { recursive: true }),
61451
+ mkdir32(join61(packageDir, "integrations"), { recursive: true })
60870
61452
  ]);
60871
61453
  await Promise.all([
60872
- writeFile25(join60(guidesDir, "AGENTS.md"), renderAgentsGuide2(input), "utf8"),
60873
- writeFile25(join60(skillsDir, "knowledge-query.md"), renderKnowledgeQuerySkill(), "utf8")
61454
+ writeFile25(join61(guidesDir, "AGENTS.md"), renderAgentsGuide2(input), "utf8"),
61455
+ writeFile25(join61(skillsDir, "knowledge-query.md"), renderKnowledgeQuerySkill(), "utf8")
60874
61456
  ]);
60875
61457
  await Promise.all(input.nodes.map(async (node2) => {
60876
61458
  const parsed = node2.located.parsed.node;
60877
- await writeFile25(join60(wikisDir, buildNodeFileName(parsed)), renderExportNodeMarkdown({ node: parsed, sections: node2.activeSections }), "utf8");
61459
+ await writeFile25(join61(wikisDir, buildNodeFileName(parsed)), renderExportNodeMarkdown({ node: parsed, sections: node2.activeSections }), "utf8");
60878
61460
  }));
60879
61461
  return {
60880
61462
  packageDir,
@@ -60956,7 +61538,7 @@ async function collectExportNodes(ctxDir) {
60956
61538
  return {
60957
61539
  config,
60958
61540
  nodes,
60959
- outputRoot: join61(ctxDir, "output")
61541
+ outputRoot: join62(ctxDir, "output")
60960
61542
  };
60961
61543
  }
60962
61544
  async function buildKnowledgePackage(input) {
@@ -61248,8 +61830,9 @@ function reconcileSchemaExample(name) {
61248
61830
  "Do not use decisions: [] to accept all defaults; an empty decisions array means no decisions were made.",
61249
61831
  "Keep proposed.content short and single-line; put long prose, code fences, and extended examples in proposed.detail.",
61250
61832
  "Do not copy basis/evidence text into proposed.detail just to show the original raw text; source_ref/source_refs already provide traceability.",
61833
+ "For kind=example, preserving the cited fenced code/config/command block in proposed.detail is active example detail, not raw-evidence echo.",
61251
61834
  'Use source_ref/source_refs copied from raw_snippets[].source_ref. Only run context source resolve-ref --node <slug> --text "<quote>" for manual recovery from a short quoted text fragment.',
61252
- "Only set decided_by: user after an actual user or tester confirmed the answer.",
61835
+ "Only set decided_by: user after an actual user or tester confirmed the answer. Auto mode or broad permission to continue is not user confirmation; if no confirmation exists, fix/split evidence or leave ask_user unresolved.",
61253
61836
  "When prepare/review returns agent_hints[], follow those result-specific hints before retrying."
61254
61837
  ],
61255
61838
  example: {
@@ -61360,6 +61943,7 @@ function compileSchemaExample(name) {
61360
61943
  "Prefer content_mode=extract when the cited raw sentence already fits as a short single-line claim. Use minimal_paraphrase only when direct extract would violate the content contract or when consolidating multiple spans; paraphrase_reason defaults to summary when omitted.",
61361
61944
  "content must be a short single-line claim of 256 characters or fewer; put long prose, code fences, and extended examples in detail.",
61362
61945
  "detail may contain active knowledge such as code/config/example material that cannot fit in content; do not use detail as a raw evidence copy or prefix it with labels such as 原文. source_refs already trace the evidence.",
61946
+ "For kind=example, preserving a cited fenced code/config/command block in detail is valid active example detail when the block is the knowledge users need to copy.",
61363
61947
  "source_refs is the only compile-draft citation input. For a single citation, use an array with one source_ref string.",
61364
61948
  "For dense raw material where one Section summarizes multiple contiguous blocks, provide all relevant source_refs. Contiguous means the selected source_refs resolve to consecutive evidence-manifest blocks from the same source, snapshot, and alias; if an intervening citation-eligible block is skipped, split the action or include that block's source_ref.",
61365
61949
  "If NodeContext only contains navigation or placeholder evidence (Parent/Children/Related/Relations, no detailed content), use op: skip; do not create a low-value description Section from those lines.",
@@ -61445,7 +62029,8 @@ function compileSchemaExample(name) {
61445
62029
  schema: "coverage-disposition",
61446
62030
  schema_version: COVERAGE_DISPOSITION_SCHEMA_VERSION,
61447
62031
  consumed_by: [
61448
- "context compile --coverage-disposition - --coverage-disposition-node <slug> --payload-digest <digest>"
62032
+ "context compile --coverage-disposition - --coverage-disposition-node <slug> --payload-digest <digest>",
62033
+ 'context compile --coverage-skip-unresolved --coverage-disposition-node <slug> --payload-digest <digest> --reason "<reason>"'
61449
62034
  ],
61450
62035
  required: ["schema_version", "dispositions"],
61451
62036
  enums: {
@@ -61455,7 +62040,8 @@ function compileSchemaExample(name) {
61455
62040
  "Read node-scoped candidates with context workflow show --payload coverage-candidates --scope <node-run-scope> --view coverage.",
61456
62041
  "Use new_section when the candidate source_refs were covered by new draft Sections.",
61457
62042
  "Use merge_into_existing or covered_by_section only with target_section_id that mechanically covers the listed source_refs.",
61458
- "Use skip with a reason when the candidate is intentionally not written for this node. For skip only, source_refs may be omitted to dispose the whole candidate."
62043
+ "Use skip with a reason when the candidate is intentionally not written for this node. For skip only, source_refs may be omitted to dispose the whole candidate.",
62044
+ "When every unresolved candidate in the node payload has the same skip rationale, prefer --coverage-skip-unresolved over hand-writing one disposition per candidate."
61459
62045
  ],
61460
62046
  example: {
61461
62047
  schema_version: COVERAGE_DISPOSITION_SCHEMA_VERSION,
@@ -61566,8 +62152,8 @@ function readPackageVersion() {
61566
62152
  try {
61567
62153
  let dir = dirname28(fileURLToPath4(import.meta.url));
61568
62154
  for (let i = 0;i < 8; i++) {
61569
- const pkg = join62(dir, "package.json");
61570
- if (existsSync48(pkg)) {
62155
+ const pkg = join63(dir, "package.json");
62156
+ if (existsSync49(pkg)) {
61571
62157
  const parsed = JSON.parse(readFileSync3(pkg, "utf8"));
61572
62158
  return parsed.version ?? "unknown";
61573
62159
  }