onto-mcp 0.4.12 → 0.4.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/.onto/authority/core-lexicon.yaml +1 -1
  2. package/.onto/authority/supported-models.yaml +137 -0
  3. package/.onto/domains/accounting-kr/competency_qs.md +153 -153
  4. package/.onto/processes/reconstruct/obligation-coverage-ledger.yaml +842 -0
  5. package/.onto/processes/reconstruct/obligation-coverage-recorded.yaml +225 -0
  6. package/.onto/processes/reconstruct/ontology-seeding-and-maturation-design.md +93 -54
  7. package/.onto/processes/reconstruct/reconstruct-contract-registry.yaml +169 -8
  8. package/.onto/processes/reconstruct/reconstruct-execution-ux-contract.md +72 -1
  9. package/.onto/processes/reconstruct/source-profiles/spreadsheet.md +35 -0
  10. package/.onto/processes/review/productized-live-path.md +9 -5
  11. package/.onto/processes/review/prompt-execution-runner-contract.md +32 -8
  12. package/.onto/processes/review/review-target-profile-contract.md +47 -4
  13. package/.onto/processes/shared/pipeline-execution-ledger-contract.md +11 -0
  14. package/.onto/processes/shared/target-material-kind-contract.md +1 -1
  15. package/README.md +64 -1
  16. package/dist/cli.js +11 -1
  17. package/dist/core-api/reconstruct-api.js +575 -24
  18. package/dist/core-api/review-api.js +70 -5
  19. package/dist/core-api/review-progress.js +7 -0
  20. package/dist/core-api/runtime-observation.js +7 -0
  21. package/dist/core-runtime/artifact-io.js +75 -3
  22. package/dist/core-runtime/cli/claude-code-review-unit-executor.js +19 -8
  23. package/dist/core-runtime/cli/materialize-review-prompt-packets.js +68 -14
  24. package/dist/core-runtime/cli/prepare-review-session.js +34 -1
  25. package/dist/core-runtime/cli/render-review-final-output.js +2 -1
  26. package/dist/core-runtime/cli/run-review-prompt-execution.js +758 -48
  27. package/dist/core-runtime/cli/run-seats.js +18 -0
  28. package/dist/core-runtime/cli/runtime-submit-context.js +10 -2
  29. package/dist/core-runtime/cli/unit-resubmit.js +269 -0
  30. package/dist/core-runtime/cli/worker-structured-output.js +80 -2
  31. package/dist/core-runtime/discovery/host-detection.js +71 -22
  32. package/dist/core-runtime/discovery/review-cert-assemble.js +208 -0
  33. package/dist/core-runtime/discovery/review-cert-record.js +442 -0
  34. package/dist/core-runtime/discovery/review-execution-units.js +22 -0
  35. package/dist/core-runtime/discovery/seat-inventory.js +167 -0
  36. package/dist/core-runtime/discovery/settings-chain.js +311 -52
  37. package/dist/core-runtime/discovery/supported-models.js +282 -29
  38. package/dist/core-runtime/discovery/synthesize-cert-assemble.js +197 -0
  39. package/dist/core-runtime/discovery/synthesize-cert-capsule.js +399 -0
  40. package/dist/core-runtime/discovery/synthesize-cert-judge.js +340 -0
  41. package/dist/core-runtime/discovery/synthesize-cert-loop.js +234 -0
  42. package/dist/core-runtime/discovery/synthesize-cert-mutation.js +117 -0
  43. package/dist/core-runtime/discovery/synthesize-cert-packet.js +182 -0
  44. package/dist/core-runtime/discovery/synthesize-cert-record.js +831 -0
  45. package/dist/core-runtime/discovery/synthesize-cert-sampler.js +336 -0
  46. package/dist/core-runtime/effort-calibration-ingest.js +135 -0
  47. package/dist/core-runtime/llm/dispatch-breaker.js +412 -0
  48. package/dist/core-runtime/llm/dispatch-fallback-adapter-capabilities.js +33 -0
  49. package/dist/core-runtime/llm/llm-caller.js +188 -47
  50. package/dist/core-runtime/llm/mock-llm-realization.js +35 -4
  51. package/dist/core-runtime/llm/model-switcher.js +1 -1
  52. package/dist/core-runtime/llm/openai-responses-incomplete-error.js +46 -0
  53. package/dist/core-runtime/llm/sealed-dispatch-capability.js +359 -0
  54. package/dist/core-runtime/llm/structured-dispatch-error.js +15 -0
  55. package/dist/core-runtime/onboard/claude-profile-scan.js +65 -0
  56. package/dist/core-runtime/onboard/cli-host.js +5 -2
  57. package/dist/core-runtime/onboard/configure-provider.js +33 -1
  58. package/dist/core-runtime/onboard/host-target.js +28 -3
  59. package/dist/core-runtime/onboard/register.js +54 -12
  60. package/dist/core-runtime/pipeline-execution-ledger.js +10 -0
  61. package/dist/core-runtime/reconstruct/artifact-types.js +35 -0
  62. package/dist/core-runtime/reconstruct/claim-projection-validation.js +17 -0
  63. package/dist/core-runtime/reconstruct/competency-projection-contract.js +63 -0
  64. package/dist/core-runtime/reconstruct/comprehension-artifact.js +321 -0
  65. package/dist/core-runtime/reconstruct/comprehension-reduce.js +367 -0
  66. package/dist/core-runtime/reconstruct/comprehension-semantic-map.js +771 -0
  67. package/dist/core-runtime/reconstruct/contract-registry.js +20 -0
  68. package/dist/core-runtime/reconstruct/dispatch-fallback-artifacts.js +640 -0
  69. package/dist/core-runtime/reconstruct/execution-telemetry.js +83 -9
  70. package/dist/core-runtime/reconstruct/final-output-sections.js +89 -0
  71. package/dist/core-runtime/reconstruct/governing-snapshot.js +36 -1
  72. package/dist/core-runtime/reconstruct/leaf-reader.js +305 -0
  73. package/dist/core-runtime/reconstruct/llm-dispatch-failure.js +270 -0
  74. package/dist/core-runtime/reconstruct/llm-touch-fingerprint.js +94 -0
  75. package/dist/core-runtime/reconstruct/material-admission-validation.js +18 -0
  76. package/dist/core-runtime/reconstruct/material-profile-validation.js +14 -0
  77. package/dist/core-runtime/reconstruct/materialize-preparation.js +304 -18
  78. package/dist/core-runtime/reconstruct/maturation-validation.js +1231 -53
  79. package/dist/core-runtime/reconstruct/mock-llm-realization.js +107 -3
  80. package/dist/core-runtime/reconstruct/obligation-assertion.js +18 -0
  81. package/dist/core-runtime/reconstruct/ontology-seed-validation.js +27 -0
  82. package/dist/core-runtime/reconstruct/output-budget.js +6 -0
  83. package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +87 -3
  84. package/dist/core-runtime/reconstruct/post-seed-validation.js +118 -5
  85. package/dist/core-runtime/reconstruct/purpose-authority-validation.js +117 -2
  86. package/dist/core-runtime/reconstruct/record.js +42 -0
  87. package/dist/core-runtime/reconstruct/registry-verification-validation.js +16 -0
  88. package/dist/core-runtime/reconstruct/run-control-validation.js +1011 -34
  89. package/dist/core-runtime/reconstruct/run.js +5198 -536
  90. package/dist/core-runtime/reconstruct/seed-authoring-readiness-validation.js +54 -3
  91. package/dist/core-runtime/reconstruct/semantic-quality-gate.js +105 -2
  92. package/dist/core-runtime/reconstruct/source-observation-delta-validation.js +41 -0
  93. package/dist/core-runtime/reconstruct/source-observations.js +166 -0
  94. package/dist/core-runtime/reconstruct/source-safety-validation.js +71 -183
  95. package/dist/core-runtime/reconstruct/source-scout-pack-validation.js +37 -1
  96. package/dist/core-runtime/reconstruct/terminal-validation.js +132 -2
  97. package/dist/core-runtime/review/continuation-plan.js +12 -5
  98. package/dist/core-runtime/review/issue-artifact-runtime.js +4 -1
  99. package/dist/core-runtime/review/materializers.js +100 -11
  100. package/dist/core-runtime/review/nesting-batch.js +6 -2
  101. package/dist/core-runtime/review/obligation-shardability.js +138 -0
  102. package/dist/core-runtime/review/pipeline-execution-ledger.js +42 -2
  103. package/dist/core-runtime/review/review-artifact-utils.js +217 -5
  104. package/dist/core-runtime/review/review-execution-steps.js +32 -8
  105. package/dist/core-runtime/review/review-progress-contract.js +8 -0
  106. package/dist/core-runtime/review/review-prompt-budget.js +97 -0
  107. package/dist/core-runtime/review/semantic-quality-gate.js +33 -7
  108. package/dist/core-runtime/review/spreadsheet-review-disposition.js +94 -0
  109. package/dist/core-runtime/route-identity.js +182 -0
  110. package/dist/core-runtime/spreadsheet-header-escalation.js +204 -0
  111. package/dist/core-runtime/spreadsheet-structure-observer.js +2689 -0
  112. package/dist/core-runtime/target-material-kind.js +58 -0
  113. package/dist/mcp/server.js +12 -5
  114. package/dist/tui/app.js +167 -0
  115. package/dist/tui/data/event-follower.js +116 -0
  116. package/dist/tui/data/node-detail.js +42 -0
  117. package/dist/tui/data/projection-poll.js +26 -0
  118. package/dist/tui/data/session-discovery.js +65 -0
  119. package/dist/tui/index.js +108 -0
  120. package/dist/tui/screens/log.js +25 -0
  121. package/dist/tui/screens/node-detail.js +17 -0
  122. package/dist/tui/screens/session-selector.js +26 -0
  123. package/dist/tui/screens/workflow-tree.js +90 -0
  124. package/dist/tui/view-model/reconstruct-adapter.js +162 -0
  125. package/dist/tui/view-model/review-adapter.js +270 -0
  126. package/dist/tui/view-model/tree-view-model.js +19 -0
  127. package/package.json +18 -3
  128. package/settings.example.json +77 -16
@@ -0,0 +1,138 @@
1
+ import { reviewMaterialGoals, } from "../target-material-kind.js";
2
+ /**
3
+ * SEALED authority: obligations whose backing evidence intrinsically spans sections
4
+ * (cross-section), so independent sharding would destroy it (🔴 ILC-2). Keyed by obligation
5
+ * identity, decoupled from the shardability declaration. Editing this set is the ONLY way to
6
+ * change an obligation's relational status — a conspicuous, test-locked change
7
+ * (obligation-shardability.invariant.test.ts fixes membership), which is exactly what makes the
8
+ * gate fail-closed against a silent relational→independent weakening.
9
+ *
10
+ * `cross_sheet_reference_integrity` is the sole relational spreadsheet obligation: its backing
11
+ * (formula_patterns[].cross_sheet_refs + cross_sheet_key_overlap) is inherently cross-sheet.
12
+ * `formula_integrity` is NOT relational — its cross-sheet evidence is owned by the separate
13
+ * `cross_sheet_reference_integrity` obligation (computeBackedGoals splits them).
14
+ */
15
+ const RELATIONAL_OBLIGATIONS = new Set([
16
+ "cross_sheet_reference_integrity",
17
+ ]);
18
+ /** True when an obligation carries cross-section evidence (sealed ground truth). Pure. */
19
+ export function isRelationalObligation(obligation) {
20
+ return RELATIONAL_OBLIGATIONS.has(obligation);
21
+ }
22
+ /** Derived projection: a mandatory cross-section seam is required to shard this obligation.
23
+ * Deterministic from `material_shardability` — never stored. */
24
+ export function requiresSeam(declaration) {
25
+ return declaration.material_shardability === "shardable_with_seam";
26
+ }
27
+ /**
28
+ * Per-obligation shardability declarations for a material kind, one-to-one with
29
+ * `reviewMaterialGoals(kind)`. Spreadsheet obligations are distilled from §3 of the design spec
30
+ * (conservative `whole` unless splitting is proven; `cross_sheet_reference_integrity` is the
31
+ * sole seam-gated relational obligation). Other kinds return `[]` until their per-material review
32
+ * adapters land — mirroring `reviewMaterialGoals`.
33
+ */
34
+ export function reviewObligationShardability(kind) {
35
+ if (kind === "spreadsheet") {
36
+ return [
37
+ { obligation: "formula_integrity", material_shardability: "whole" },
38
+ {
39
+ obligation: "cross_sheet_reference_integrity",
40
+ material_shardability: "shardable_with_seam",
41
+ },
42
+ { obligation: "named_range_hygiene", material_shardability: "shardable_independent" },
43
+ { obligation: "data_validation_coverage", material_shardability: "shardable_independent" },
44
+ { obligation: "access_and_protection_hygiene", material_shardability: "whole" },
45
+ { obligation: "structural_risk_signals", material_shardability: "whole" },
46
+ ];
47
+ }
48
+ return [];
49
+ }
50
+ /**
51
+ * Pure validator core (git-free, injectable — mirrors check-obligation-coverage's
52
+ * `evaluateObligationCoverage`). Returns every violation; `[]` means the declarations are
53
+ * exhaustive AND consistent with the sealed relational authority. Relationality is read from the
54
+ * module's sealed `isRelationalObligation` — NOT from the (injectable) declarations — so a
55
+ * crafted declaration cannot launder its own relational status. Enforces INV-SHARD-1.
56
+ */
57
+ export function evaluateObligationShardability(inputs) {
58
+ const violations = [];
59
+ const obligationSet = new Set(inputs.obligations);
60
+ const declarations = inputs.declarations;
61
+ // (1) exhaustiveness: no orphan / duplicate declarations.
62
+ const seen = new Set();
63
+ for (const declaration of declarations) {
64
+ if (!obligationSet.has(declaration.obligation)) {
65
+ violations.push({
66
+ obligation: declaration.obligation,
67
+ kind: "orphan_declaration",
68
+ detail: "declaration for an obligation not in the catalog",
69
+ });
70
+ continue;
71
+ }
72
+ if (seen.has(declaration.obligation)) {
73
+ violations.push({
74
+ obligation: declaration.obligation,
75
+ kind: "duplicate_declaration",
76
+ detail: "obligation declared more than once",
77
+ });
78
+ continue;
79
+ }
80
+ seen.add(declaration.obligation);
81
+ // (2) relational fail-closed: a relational obligation must not be shardable_independent.
82
+ if (isRelationalObligation(declaration.obligation) &&
83
+ declaration.material_shardability === "shardable_independent") {
84
+ violations.push({
85
+ obligation: declaration.obligation,
86
+ kind: "relational_independent",
87
+ detail: "relational obligation declared shardable_independent — would destroy cross-section evidence (ILC-2)",
88
+ });
89
+ }
90
+ // (3) seam is only for relational obligations.
91
+ if (declaration.material_shardability === "shardable_with_seam" &&
92
+ !isRelationalObligation(declaration.obligation)) {
93
+ violations.push({
94
+ obligation: declaration.obligation,
95
+ kind: "seam_on_local",
96
+ detail: "non-relational obligation declared shardable_with_seam — a seam is only for relational evidence",
97
+ });
98
+ }
99
+ }
100
+ // (1, cont.) every obligation has a declaration.
101
+ for (const obligation of inputs.obligations) {
102
+ if (!seen.has(obligation)) {
103
+ violations.push({
104
+ obligation,
105
+ kind: "missing_declaration",
106
+ detail: "obligation in the catalog has no shardability declaration",
107
+ });
108
+ }
109
+ }
110
+ return violations;
111
+ }
112
+ /**
113
+ * fail-closed validator (the immediate consumer of the shardability declarations, so they are
114
+ * never a consumer-less dead struct). Wraps the pure core with the real per-kind catalog and
115
+ * declarations. `[]` for every kind is the Stage 2 lock (asserted by the G3 invariant test).
116
+ */
117
+ export function validateObligationShardability(kind) {
118
+ return evaluateObligationShardability({
119
+ obligations: reviewMaterialGoals(kind),
120
+ declarations: reviewObligationShardability(kind),
121
+ });
122
+ }
123
+ /**
124
+ * Pure shardability gate (Stage 3 calls this; no consumer at Stage 2 — a pure function, not a
125
+ * data struct, so not a dead struct). A shard is permitted only when the obligation is not
126
+ * `whole`, the proposed shard keeps each element intact, and either it is independently
127
+ * shardable or its mandatory cross-section seam is actually covered. (design §5.3.)
128
+ */
129
+ export function isObligationShardable(args) {
130
+ const { declaration, seam_covered, element_intact } = args;
131
+ if (declaration.material_shardability === "whole")
132
+ return false;
133
+ if (!element_intact)
134
+ return false;
135
+ if (declaration.material_shardability === "shardable_independent")
136
+ return true;
137
+ return seam_covered; // shardable_with_seam
138
+ }
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import { deliberationResolutionPath, issueDeliberationPromptPacketPath, issueDeliberationResponsePath, } from "./controlled-lens-deliberation.js";
3
3
  import { synthesisLedgerPath, synthesisWorkItemsPath, } from "./synthesis-map-reduce.js";
4
4
  import { fileExists, readYamlDocument } from "./review-artifact-utils.js";
5
- import { PIPELINE_EXECUTION_LEDGER_SCHEMA_VERSION, buildLedgerTrust, buildOutputHashes, fileSha256IfPresent, isTrustedLedgerUnit, normalizeLedgerRefs, } from "../pipeline-execution-ledger.js";
5
+ import { PIPELINE_EXECUTION_LEDGER_SCHEMA_VERSION, buildLedgerTrust, buildOutputHashes, fileSha256IfPresent, isResolvedLedgerUnit, normalizeLedgerRefs, } from "../pipeline-execution-ledger.js";
6
6
  const PRE_DELIBERATION_ISSUE_ARTIFACT_ORDER = [
7
7
  "finding-ledger",
8
8
  "finding-relation-graph",
@@ -442,6 +442,32 @@ async function buildUnitEntry(args) {
442
442
  downstreamUnitIds: args.downstreamUnitIds,
443
443
  };
444
444
  }
445
+ /**
446
+ * 설계 A demotion-disclosure consumer: lenses listed in the persisted stance
447
+ * matrix's `validation.missing_stances` were demoted complete-with-failure
448
+ * after bounded resubmit exhaustion; the matrix consumed the gap, so their
449
+ * per-lens ledger units are terminally resolved (owe no further dispatch).
450
+ */
451
+ async function demotedStanceUnitIdsFromMatrix(executionPlan) {
452
+ const matrixPath = executionPlan.issue_stance_matrix_path;
453
+ if (!matrixPath || !(await fileExists(matrixPath)))
454
+ return new Set();
455
+ try {
456
+ const matrix = await readYamlDocument(matrixPath);
457
+ return new Set((matrix.validation?.missing_stances ?? [])
458
+ .map((entry) => entry?.lens_id)
459
+ .filter((lensId) => typeof lensId === "string")
460
+ .map((lensId) => `issue-stance:${lensId}`));
461
+ }
462
+ catch (error) {
463
+ // The matrix's `validation.missing_stances` is the only durable authority
464
+ // for stance demotion. A present-but-unreadable/unparseable matrix (torn
465
+ // write, out-of-band corruption) must fail loud instead of silently
466
+ // returning an empty set — a silent empty set would drop the demotion
467
+ // signal and regress the affected stance unit(s) to unresolved/blocked.
468
+ throw new Error(`Failed to read demoted stance units from issue-stance matrix at ${matrixPath}: ${error instanceof Error ? error.message : String(error)}`);
469
+ }
470
+ }
445
471
  export async function buildReviewPipelineExecutionLedger(params) {
446
472
  const [deliberationDiskUnits, synthesisDiskUnits] = await Promise.all([
447
473
  deliberationUnitsFromDisk(params.executionPlan),
@@ -471,6 +497,10 @@ export async function buildReviewPipelineExecutionLedger(params) {
471
497
  const workerUnitsByUnitId = firstWinsByUnitId((params.reviewRunManifest?.worker_units ?? []).filter((unit) => typeof unit.unit_id === "string"), (unit) => unit.unit_id);
472
498
  const trustedUnitIds = new Set();
473
499
  const ledgerUnits = [];
500
+ // Lazy + memoized: the matrix read matters only when some stance unit is
501
+ // not completed; a healthy run (frontier loops call this builder per
502
+ // iteration) never pays the stat+read+parse.
503
+ let demotedStanceUnitIds = null;
474
504
  for (const plannedUnit of units) {
475
505
  const entry = await buildUnitEntry({
476
506
  plannedUnit,
@@ -484,8 +514,18 @@ export async function buildReviewPipelineExecutionLedger(params) {
484
514
  hasReviewRunManifest: params.reviewRunManifest !== null && params.reviewRunManifest !== undefined,
485
515
  trustedUnitIds,
486
516
  });
517
+ if (plannedUnit.unitId.startsWith("issue-stance:") &&
518
+ entry.status !== "completed") {
519
+ demotedStanceUnitIds ??= await demotedStanceUnitIdsFromMatrix(params.executionPlan);
520
+ if (demotedStanceUnitIds.has(plannedUnit.unitId)) {
521
+ entry.resolution = "demoted";
522
+ }
523
+ }
487
524
  ledgerUnits.push(entry);
488
- if (isTrustedLedgerUnit(entry))
525
+ // Trusted output or terminally resolved — a demoted unit satisfies
526
+ // downstream upstream-trust because the stage product consumed the
527
+ // disclosed gap, not the unit's (absent) output.
528
+ if (isResolvedLedgerUnit(entry))
489
529
  trustedUnitIds.add(entry.unitId);
490
530
  }
491
531
  return {
@@ -2,6 +2,8 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import YAML from "yaml";
4
4
  import { atomicWriteFile } from "../artifact-io.js";
5
+ import { isSpreadsheetRef } from "../target-material-kind.js";
6
+ import { DEFAULT_WORKBOOK_INVENTORY_PROMPT_CAPS, observeSpreadsheetSource, projectInventoryForAdmission, projectInventoryForPrompt, } from "../spreadsheet-structure-observer.js";
5
7
  export const DEFAULT_EXCLUDED_NAMES = [
6
8
  ".git",
7
9
  "node_modules",
@@ -174,9 +176,219 @@ export async function collectFilePathsRecursively(rootPath, options, currentDept
174
176
  }
175
177
  return collected.sort();
176
178
  }
177
- export async function readTextOrDirectoryListing(targetPath, options) {
179
+ /**
180
+ * Render the workbook inventory as a compact text view for the review prompt
181
+ * (design §3.2 — the "inventory projection (text view)"). Honesty
182
+ * (review-target-profile §6): structure is inspected only, cell values and formula
183
+ * results are not evaluated.
184
+ *
185
+ * The view carries bounded DETAIL — not just counts — for every section that backs a
186
+ * kind-derived review obligation (`reviewMaterialGoals`): formula TEXT + cross-sheet
187
+ * refs, named-range refers_to, data-validation rule_summary, error-cell tokens, and
188
+ * external-link targets. Without the detail those obligations would be unverifiable
189
+ * (the reviewer would be told to audit content that is only an integer in the prompt).
190
+ * Two projections compose: `projectInventoryForAdmission` drops raw cell values, then
191
+ * `projectInventoryForPrompt` bounds array SIZE so a large workbook cannot overflow the
192
+ * prompt. Anything trimmed is surfaced honestly (the "structural sample bounded" line);
193
+ * the full detail stays in the persisted inventory.
194
+ */
195
+ function renderSpreadsheetStructuralView(inventory, caps) {
196
+ const lines = [
197
+ "[Spreadsheet Structural Inventory — structure inspected only; cell values and formula results are not evaluated]",
198
+ `workbook_kind: ${inventory.workbook_kind}`,
199
+ `content_sha256: ${inventory.content_sha256}`,
200
+ ];
201
+ if (inventory.unsupported_reason) {
202
+ lines.push(`unsupported: ${inventory.unsupported_reason}`);
203
+ return `${lines.join("\n")}\n`;
204
+ }
205
+ // Stage 1: when the caller threads window-proportional caps, size the prompt
206
+ // projection to the lens model's window; otherwise the DEFAULT caps keep the
207
+ // render byte-identical to today (no regression).
208
+ const { inventory: inv, sections } = projectInventoryForPrompt(projectInventoryForAdmission(inventory), caps ?? DEFAULT_WORKBOOK_INVENTORY_PROMPT_CAPS);
209
+ // One rendered formula PATTERN line (Stage 1.1): the deduped formula TEXT + how many
210
+ // cells carry it (occurrence_count) + a representative cell + the sheets it spans + any
211
+ // cross-sheet references — the obligation-backing detail for formula_integrity /
212
+ // cross_sheet_reference_integrity (a count alone is not auditable). A fill-down that was
213
+ // tens of thousands of per-cell rows now renders as one auditable line.
214
+ const renderFormulaPatternLine = (pattern) => {
215
+ const xref = pattern.cross_sheet_refs.length > 0
216
+ ? ` [cross-sheet → ${pattern.cross_sheet_refs.join(", ")}]`
217
+ : "";
218
+ return (` - ${pattern.pattern} (×${pattern.occurrence_count}, e.g. ${pattern.sample_cell}` +
219
+ `; sheets: ${pattern.sheets.join(", ")})${xref}`);
220
+ };
221
+ // Friendly labels for the bounded-sample note. A3: count-only sections
222
+ // (tables/merged_ranges, printed as full counts below) are SUPPRESSED from the note so it
223
+ // never contradicts them (RC-2); every OTHER projector-recorded trim is disclosed —
224
+ // friendly label when known, else the raw section key — so a newly-added
225
+ // projectInventoryForPrompt section can never be silently dropped from disclosure (#4 class).
226
+ const NOTE_SECTION_LABELS = {
227
+ per_sheet_data: "sheet bodies",
228
+ "per_sheet_data.columns": "columns",
229
+ distinct_value_vocab: "distinct-value columns",
230
+ formula_patterns: "formula patterns",
231
+ named_ranges: "named_ranges",
232
+ data_validations: "data_validations",
233
+ external_links: "external_links",
234
+ error_cells: "error_cells",
235
+ pivot_tables: "pivot_tables",
236
+ cross_sheet_key_overlap: "cross_sheet_key_overlap",
237
+ "cross_sheet_key_overlap.pairwise_overlap": "cross-sheet pairwise overlaps",
238
+ risk_signals: "risk_signals",
239
+ };
240
+ const COUNT_ONLY_TRIM_SECTIONS = new Set(["tables", "merged_ranges"]);
241
+ const trimSectionLabel = (section) => NOTE_SECTION_LABELS[section] ?? section;
242
+ const renderedTrims = sections.filter((s) => !COUNT_ONLY_TRIM_SECTIONS.has(s.section));
243
+ // Total sheet count comes from the FULL inventory; the per-sheet layout below renders only
244
+ // the sheets whose (capped) per_sheet_data body survived projection.
245
+ // When observation was bounded by the sheet cap, sheet_count_total holds the true total —
246
+ // render "N of M observed" so the count is not mis-reported as the post-cap number, and a
247
+ // reviewer knows beyond-cap sheets (and any protection/structure on them) were not observed.
248
+ const sheetCountLabel = inventory.sheet_count_total !== undefined
249
+ ? `${inventory.sheets.length} of ${inventory.sheet_count_total} observed`
250
+ : `${inventory.sheets.length}`;
251
+ lines.push(`sheets: ${sheetCountLabel}${inv.capture_truncated ? " (capture truncated)" : ""}`);
252
+ if (renderedTrims.length > 0) {
253
+ // Honest about WHAT this note means: review renders a bounded PROMPT sample and does not
254
+ // persist the full WorkbookStructuralInventory, so the trimmed detail is not separately
255
+ // recoverable — never imply the dropped rows can be retrieved later (R1).
256
+ lines.push(`structural sample bounded (prompt sample only — review does not persist the full inventory): ${renderedTrims
257
+ .map((s) => `${trimSectionLabel(s.section)} ${s.kept}/${s.total}`)
258
+ .join(", ")}`);
259
+ }
260
+ const renderedSheetNames = new Set(inv.per_sheet_data.map((d) => d.sheet));
261
+ // B3: protected/hidden sheets beyond the per_sheet_data render cap keep
262
+ // access_and_protection_hygiene attached, but their flags are emitted only inside the
263
+ // per-sheet layout below (rendered-sheet set) — disclose the beyond-cap count so a
264
+ // reviewer does not read the silence as "the rest are unprotected/visible".
265
+ const beyondCapAccessFlags = inventory.sheets.filter((sheet) => !renderedSheetNames.has(sheet.name) && (sheet.protected || sheet.hidden));
266
+ if (beyondCapAccessFlags.length > 0) {
267
+ lines.push(`${beyondCapAccessFlags.length} protected/hidden sheet(s) beyond the rendered sample (flags not individually shown; access_and_protection_hygiene still applies)`);
268
+ }
269
+ // B2: obligation-backing detail is emitted UP FRONT — before the per-sheet bodies — so the
270
+ // downstream 300-line embed head-cut (truncateForEmbedding) preserves the evidence backing
271
+ // named_range_hygiene / data_validation_coverage / structural_risk_signals /
272
+ // cross_sheet_reference_integrity even for a many-sheet workbook. Reordering alone is
273
+ // zero-sum under a fixed budget, so each obligation class gets a bounded in-band sample
274
+ // here; the per-sheet layout (columns) follows and may be truncated under heavy load
275
+ // without dropping any obligation backing.
276
+ if (inventory.named_ranges.length) {
277
+ lines.push("", `named_ranges: ${inventory.named_ranges.length}`);
278
+ for (const nr of inv.named_ranges) {
279
+ lines.push(` - ${nr.name} (${nr.scope}) → ${nr.refers_to}`);
280
+ }
281
+ }
282
+ if (inventory.data_validations.length) {
283
+ lines.push("", `data_validations: ${inventory.data_validations.length}`);
284
+ for (const dv of inv.data_validations) {
285
+ // design-C: surface the DECLARED type=list enum members HERE (this loop runs over EVERY
286
+ // validation, independent of per-sheet column/sheet rendering) so they reach the prompt even
287
+ // when the column is trimmed by the column cap, the sheet is dropped by the sheet cap, the
288
+ // sheet has no profiled columns, or one column carries multiple disjoint list validations
289
+ // (Codex round3 #2/#3/#4/#7). Members are DECLARED labels (bounded), not observed data.
290
+ const enumLabels = dv.members && dv.members.length > 0 ? ` enum=[${dv.members.join(", ")}]` : "";
291
+ lines.push(` - ${dv.sheet}!${dv.range}: ${dv.rule_summary}${enumLabels}`);
292
+ }
293
+ }
294
+ if (inventory.error_cells.length) {
295
+ lines.push("", `error_cells: ${inventory.error_cells.length}`);
296
+ for (const ec of inv.error_cells) {
297
+ lines.push(` - ${ec.sheet}!${ec.cell}: ${ec.token}`);
298
+ }
299
+ }
300
+ if (inventory.external_links.length) {
301
+ lines.push("", `external_links: ${inventory.external_links.length}`);
302
+ for (const el of inv.external_links) {
303
+ lines.push(` - ${el.kind}: ${el.target}`);
304
+ }
305
+ }
306
+ // Remaining structural facts that are obligation context but not per-item audit detail.
307
+ const counts = [];
308
+ if (inventory.tables.length)
309
+ counts.push(`tables: ${inventory.tables.length}`);
310
+ if (inventory.merged_ranges.length)
311
+ counts.push(`merged_ranges: ${inventory.merged_ranges.length}`);
312
+ if (inventory.macro_present)
313
+ counts.push("macro_present: true");
314
+ if (counts.length)
315
+ lines.push("", counts.join("; "));
316
+ if (inventory.pivot_tables.length) {
317
+ lines.push("", `pivot_tables: ${inventory.pivot_tables.length}`);
318
+ for (const p of inv.pivot_tables) {
319
+ const src = p.source_sheet ? ` source=${p.source_sheet}!${p.source_ref ?? ""}` : "";
320
+ lines.push(` - ${p.name} @ ${p.sheet}!${p.location}${src}`);
321
+ lines.push(` rows=[${p.row_fields.join(", ")}] cols=[${p.column_fields.join(", ")}]` +
322
+ ` data=[${p.data_fields.join(", ")}] filters=[${p.page_fields.join(", ")}]`);
323
+ }
324
+ }
325
+ if (inventory.cross_sheet_key_overlap.length) {
326
+ lines.push("", "cross_sheet_key_overlap (shared-column value overlap, counts only):");
327
+ for (const o of inv.cross_sheet_key_overlap) {
328
+ const pairs = o.pairwise_overlap.map((p) => `${p.a}∩${p.b}=${p.count}`).join(", ");
329
+ lines.push(` - ${o.key_name} across [${o.sheets.join(", ")}]: ${pairs}`);
330
+ }
331
+ }
332
+ if (inventory.risk_signals.length) {
333
+ lines.push("", "risk_signals:");
334
+ for (const signal of inv.risk_signals) {
335
+ lines.push(` - ${signal.kind} @ ${signal.location}: ${signal.literal}`);
336
+ }
337
+ }
338
+ // formula_integrity / cross_sheet_reference_integrity backing: the deduplicated formula
339
+ // PATTERNS (Stage 1.1), emitted up front (before the per-sheet bodies) so the formula TEXT
340
+ // survives the embed cut. A fill-down (one master replicated across N cells) is one pattern
341
+ // here; formula_cells_total reports the honest cell count (lower-bound when the distinct-
342
+ // pattern cap dropped a new pattern), so the reviewer sees both the distinct logic and its
343
+ // true reach. Each pattern carries the sheets it spans, so a workbook whose formulas live
344
+ // only on a trimmed sheet still shows its formula text.
345
+ if (inv.formula_patterns.length > 0) {
346
+ const lowerBound = inv.formula_cells_total_is_lower_bound ? ", lower-bound" : "";
347
+ lines.push("", `formulas: ${inv.formula_patterns.length} distinct patterns over ${inv.formula_cells_total} cells${lowerBound}:`);
348
+ for (const pattern of inv.formula_patterns) {
349
+ lines.push(renderFormulaPatternLine(pattern));
350
+ }
351
+ }
352
+ // Per-sheet LAYOUT (dimensions + columns) — context that follows the obligation backing
353
+ // above and may be truncated under heavy load without losing any obligation evidence.
354
+ for (const sheet of inv.sheets.filter((s) => renderedSheetNames.has(s.name))) {
355
+ const flags = `${sheet.hidden ? " (hidden)" : ""}${sheet.protected ? " (protected)" : ""}`;
356
+ lines.push("", `## sheet: ${sheet.name}`, `dimensions: ${sheet.dimensions.rows} rows × ${sheet.dimensions.cols} cols${flags}`);
357
+ const data = inv.per_sheet_data.find((d) => d.sheet === sheet.name);
358
+ if (data) {
359
+ const lowConfidence = data.header_confidence === "low" ? "; header_confidence: low (layout uncertain)" : "";
360
+ lines.push(`layout_kind: ${data.layout_kind}; header_rows: ${data.header_rows ? data.header_rows.join(",") : "none"}${lowConfidence}`);
361
+ if (data.columns.length > 0) {
362
+ lines.push("columns:");
363
+ for (const col of data.columns) {
364
+ const vocab = inv.distinct_value_vocab.find((v) => v.sheet === sheet.name && v.column === col.name);
365
+ const distinct = vocab
366
+ ? `; distinct≈${vocab.distinct_count}${vocab.distinct_count_is_estimate ? "+" : ""}`
367
+ : "";
368
+ // design-C per-column cardinality: distinct/non_empty COUNT over scanned rows. Declared
369
+ // type=list enum members are surfaced in the data_validations section (above), NOT here —
370
+ // so they survive column/sheet trimming and multi-validation columns (Codex round3).
371
+ const cardinality = `; cardinality=${col.distinct_count}${col.distinct_count_is_estimate ? "+" : ""}/${col.non_empty_count}`;
372
+ lines.push(` - ${col.name} (${col.inferred_type}; non_empty=${col.non_empty_ratio.toFixed(2)}${cardinality}${distinct})`);
373
+ }
374
+ }
375
+ }
376
+ }
377
+ return `${lines.join("\n")}\n`;
378
+ }
379
+ export async function readTextOrDirectoryListing(targetPath, options, inventory, inventoryPromptCaps) {
178
380
  const stats = await fs.stat(targetPath);
179
381
  if (!stats.isDirectory()) {
382
+ // Spreadsheet targets are rendered as a structural/aggregate inventory view,
383
+ // not raw bytes (design §3.2). review routes through the SAME shared projection
384
+ // as reconstruct — raw cell values never enter the prompt, and a binary
385
+ // workbook is never dumped as garbage utf8. When the orchestrator already
386
+ // observed this ref (single-observation, design §3.2), reuse that inventory;
387
+ // otherwise observe here. The render applies the admission + prompt projections,
388
+ // sized by the optional Stage 1 window-proportional caps (undefined → DEFAULT).
389
+ if (isSpreadsheetRef(targetPath)) {
390
+ return renderSpreadsheetStructuralView(inventory ?? (await observeSpreadsheetSource(targetPath)), inventoryPromptCaps);
391
+ }
180
392
  try {
181
393
  return await fs.readFile(targetPath, "utf8");
182
394
  }
@@ -208,20 +420,20 @@ export async function readTextOrDirectoryListing(targetPath, options) {
208
420
  : "";
209
421
  return `[Directory Listing]\n${listing}\n${truncationNote}`;
210
422
  }
211
- export async function renderTargetSnapshot(resolvedTargetRefs, options) {
423
+ export async function renderTargetSnapshot(resolvedTargetRefs, options, inventoryByRef, inventoryPromptCaps) {
212
424
  const sections = [];
213
425
  for (const resolvedTargetRef of resolvedTargetRefs) {
214
- sections.push(`## ${resolvedTargetRef}`, "", await readTextOrDirectoryListing(resolvedTargetRef, options), "");
426
+ sections.push(`## ${resolvedTargetRef}`, "", await readTextOrDirectoryListing(resolvedTargetRef, options, inventoryByRef?.get(path.resolve(resolvedTargetRef)), inventoryPromptCaps), "");
215
427
  }
216
428
  return `${sections.join("\n").trimEnd()}\n`;
217
429
  }
218
- export async function renderReviewTargetMaterializedInput(materializedKind, materializedRefs, options) {
430
+ export async function renderReviewTargetMaterializedInput(materializedKind, materializedRefs, options, inventoryByRef, inventoryPromptCaps) {
219
431
  const sections = [`kind: ${materializedKind}`, ""];
220
432
  for (const materializedRef of materializedRefs) {
221
433
  sections.push(`## ${path.basename(materializedRef)}`);
222
434
  sections.push(`ref: ${materializedRef}`);
223
435
  sections.push("");
224
- sections.push(await readTextOrDirectoryListing(materializedRef, options));
436
+ sections.push(await readTextOrDirectoryListing(materializedRef, options, inventoryByRef?.get(path.resolve(materializedRef)), inventoryPromptCaps));
225
437
  sections.push("");
226
438
  }
227
439
  return `${sections.join("\n").trimEnd()}\n`;
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { fileExists, isoNow, readYamlDocument, writeYamlDocument, } from "./review-artifact-utils.js";
4
4
  import { buildReviewPipelineExecutionLedger, } from "./pipeline-execution-ledger.js";
5
- import { fileSha256IfPresent, isTrustedLedgerUnit, } from "../pipeline-execution-ledger.js";
5
+ import { fileSha256IfPresent, isResolvedLedgerUnit, isTrustedLedgerUnit, } from "../pipeline-execution-ledger.js";
6
6
  import { buildReviewContinuationPlan, } from "./continuation-plan.js";
7
7
  import { readValidatedLensSidecarArtifact } from "./lens-sidecar-artifact.js";
8
8
  import { computeLensCompletionBarrier, resolveRequiredParticipatingLensCount, } from "./lens-completion-policy.js";
@@ -30,6 +30,23 @@ import { renderReviewUnitBoundaryDetailsSection } from "./unit-boundary-details.
30
30
  * step calls `completeReviewSession`, which lives in the cli layer, so the
31
31
  * core-api wrapper runs it when reviewAdvance signals `ready_to_assemble`.
32
32
  */
33
+ /**
34
+ * Single-source runtime mirror of the closed {@link ReviewUnitKind} union. The
35
+ * `satisfies Record<ReviewUnitKind, true>` makes adding an enum member a compile
36
+ * error here, so a new kind cannot be planned/executed until it is wired into the
37
+ * result buckets ({@link mergeUnitResultIntoExecutionResult}) and the seat gate.
38
+ * This is the guard against the silent-drop where an unhandled unit_kind would
39
+ * leave the frontier permanently stalled with no error.
40
+ */
41
+ const REVIEW_UNIT_KINDS = {
42
+ lens: true,
43
+ issue_artifact: true,
44
+ deliberation: true,
45
+ synthesize: true,
46
+ };
47
+ function isReviewUnitKind(value) {
48
+ return Object.prototype.hasOwnProperty.call(REVIEW_UNIT_KINDS, value);
49
+ }
33
50
  async function readOptionalYamlArtifact(filePath) {
34
51
  if (!(await fileExists(filePath)))
35
52
  return null;
@@ -136,6 +153,9 @@ export async function computeReviewFrontier(sessionRoot, targetUnits) {
136
153
  */
137
154
  export async function validateUnitSeatToResult(args) {
138
155
  const { unit } = args;
156
+ if (!isReviewUnitKind(unit.unitKind)) {
157
+ throw new Error(`validateUnitSeatToResult: unit ${unit.unitId} has unknown unit_kind "${unit.unitKind}" (not a ReviewUnitKind); a new kind must be wired into the result buckets and frontier routing before it can be executed.`);
158
+ }
139
159
  const executionPlan = args.executionPlan ?? (await loadExecutionPlan(args.sessionRoot));
140
160
  const recordedAt = args.recordedAt ?? isoNow();
141
161
  const baseResult = (extra) => ({
@@ -251,8 +271,10 @@ export async function mergeUnitResultIntoExecutionResult(args) {
251
271
  ...artifact,
252
272
  synthesis_map_execution_results: upsertUnitResult(artifact.synthesis_map_execution_results ?? [], result),
253
273
  };
254
- default:
255
- return artifact;
274
+ default: {
275
+ const exhaustive = result.unit_kind;
276
+ throw new Error(`mergeUnitResultIntoExecutionResult: no execution-result bucket for unit_kind "${String(exhaustive)}" (unit ${result.unit_id}); extend this switch when adding a ReviewUnitKind.`);
277
+ }
256
278
  }
257
279
  })();
258
280
  await writeYamlDocument(resultPath, merged);
@@ -829,7 +851,8 @@ async function ensureDeliberationMarkdownProjection(sessionRoot, plan) {
829
851
  */
830
852
  async function finalizeHostExecutionResultIfComplete(sessionRoot, plan) {
831
853
  const ledger = await buildSessionLedger(sessionRoot, plan);
832
- if (!ledger.units.every((unit) => isTrustedLedgerUnit(unit)))
854
+ // Convergence = no owed work: trusted OR terminally resolved (demoted).
855
+ if (!ledger.units.every((unit) => isResolvedLedgerUnit(unit)))
833
856
  return;
834
857
  const resultPath = executionResultPath(sessionRoot);
835
858
  const existing = await readOptionalYamlArtifact(resultPath);
@@ -874,10 +897,11 @@ async function finalizeHostExecutionResultIfComplete(sessionRoot, plan) {
874
897
  */
875
898
  async function computeRoundResult(sessionRoot, plan) {
876
899
  const frontier = await computeReviewFrontier(sessionRoot);
877
- // Terminal: every ledger unit is trusted, so there is nothing left to run and
878
- // the caller assembles. (A continuation plan with no untrusted frontier is
879
- // `eligible: false`, so this must precede the ineligibility check.)
880
- if (frontier.unitLedger.units.every((unit) => isTrustedLedgerUnit(unit))) {
900
+ // Terminal: every ledger unit owes no work (trusted or terminally
901
+ // resolved), so there is nothing left to run and the caller assembles.
902
+ // (A continuation plan with no untrusted frontier is `eligible: false`,
903
+ // so this must precede the ineligibility check.)
904
+ if (frontier.unitLedger.units.every((unit) => isResolvedLedgerUnit(unit))) {
881
905
  return { status: "ready_to_assemble" };
882
906
  }
883
907
  if (!frontier.eligible) {
@@ -105,6 +105,10 @@ export function reviewProgressStepIdFromHalt(args) {
105
105
  if (args.haltPhase === "lens_completion_barrier") {
106
106
  return "lens_completion_barrier";
107
107
  }
108
+ // 설계 B: lens 풀 dispatch breaker 트립은 lens 디스패치 단계에서 멈춘
109
+ // 것이다 (배리어 판정 이전; halt_unit_kind는 null로 온다).
110
+ if (args.haltPhase === "lens_dispatch_breaker")
111
+ return "lens_dispatch";
108
112
  if (args.haltPhase === "synthesize")
109
113
  return "synthesize";
110
114
  if (args.haltPhase === "controlled_lens_deliberation") {
@@ -114,6 +118,10 @@ export function reviewProgressStepIdFromHalt(args) {
114
118
  }
115
119
  if (args.haltPhase === "issue_artifact") {
116
120
  const unitId = args.haltUnitId;
121
+ // 동적 per-lens stance 유닛(issue-stance:<lens>)은 issue-stance-matrix
122
+ // 산출 단계에 속한다 — issue_artifact_id 정확 일치로는 매핑되지 않는다.
123
+ if (unitId?.startsWith("issue-stance:"))
124
+ return "issue_stance_matrix";
117
125
  const step = REVIEW_PROGRESS_STEPS.find((candidate) => candidate.issue_artifact_id === unitId);
118
126
  return step?.id ?? "finding_ledger";
119
127
  }