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
@@ -3,7 +3,9 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { atomicWriteYamlDocument as writeYamlDocument } from "../artifact-io.js";
5
5
  import { aggregateTargetMaterialDetections, detectTargetMaterialRefs, } from "../target-material-kind.js";
6
+ import { observeSpreadsheetSource, projectInventoryForAdmission, SPREADSHEET_CAPTURE_TRUNCATED_PHRASE, SPREADSHEET_MACRO_PRESENT_PHRASE, SPREADSHEET_OBSERVER_ADAPTER_ID, } from "../spreadsheet-structure-observer.js";
6
7
  import { validateSourceObservationBoundary, } from "./source-observations.js";
8
+ import { buildDeterministicComprehensionArtifact } from "./comprehension-artifact.js";
7
9
  import { loadReconstructSourceProfiles, } from "./source-profiles.js";
8
10
  const CONCRETE_TARGET_MATERIAL_KINDS = new Set([
9
11
  "code",
@@ -111,28 +113,173 @@ function defaultProfileForKind(profiles, kind) {
111
113
  matchingProfiles[0];
112
114
  }
113
115
  /**
114
- * Structural `content_excerpt` capture budgets (chars). Code and other kinds keep
115
- * a small leading-sample budget the observation is a structural sample, not the
116
- * whole file. Text-readable document prose is captured whole so the document tail
117
- * (goals, milestones, decisions) reaches seed authoring instead of being lost to a
118
- * leading slice: a typical business/policy document is well within any modern model
119
- * window (e.g. ~12.5K chars ≈ ~3K tokens). The 200K cap is a safety ceiling against
120
- * pathological inputs; documents that genuinely exceed it are decomposed by a later
121
- * stage (see development-records/design/20260616-large-input-observation), not
122
- * silently truncated here.
116
+ * Structural `content_excerpt` capture budgets (chars). Code source and text-readable
117
+ * document prose are captured whole so the file tail (later definitions, document
118
+ * goals/milestones/decisions) reaches seed authoring instead of being lost to a
119
+ * leading slice both are projected whole at the seed stage, so a small capture
120
+ * would silently author the seed from a partial file. Other kinds (binary documents,
121
+ * spreadsheet/database structural inventories) keep a small leading sample the
122
+ * observation is a structural sample there, not the whole file.
123
+ *
124
+ * Capture is MODEL-AGNOSTIC: it runs before the seed-stage (provider, model) is
125
+ * resolved, so it captures up to a fixed `DOCUMENT_CAPTURE_CEILING_CHARS` ceiling
126
+ * (a disk/pathological-input safety bound, NOT a window bound). The seed-stage
127
+ * prompt PROJECTION then slices that captured prose to a dynamic, model-aware
128
+ * budget (see deriveDocumentExcerptProjectionBudget + run.ts). The ceiling is set
129
+ * comfortably above any projection budget so projection only ever narrows, never
130
+ * needs more than was captured. A document longer than the ceiling is captured-
131
+ * truncated (`excerpt_truncated`); one longer than the projection budget is
132
+ * projection-truncated at the seed stage (recorded durably there). True
133
+ * window-overflow recovery (selecting the relevant tail) is a later stage (see
134
+ * development-records/design/20260616-large-input-observation).
123
135
  *
124
136
  * Material kind is detected by extension (target-material-kind.ts `DOCUMENT_EXTENSIONS`),
125
137
  * and `document` includes binary formats (.pdf/.docx/.ppt/.rtf) that `textStats` still
126
138
  * reads as UTF-8. Only text-readable document formats earn the whole-document budget —
127
- * a binary document keeps the small sample so we never capture 200K of decoded binary
128
- * bytes and spend the prompt on garbage; binary documents need an extraction step before
129
- * reconstruct. This set must be the text-prose subset of `DOCUMENT_EXTENSIONS`: an
139
+ * a binary document keeps the small sample so we never capture the ceiling in decoded
140
+ * binary bytes and spend the prompt on garbage; binary documents need an extraction step
141
+ * before reconstruct. This set must be the text-prose subset of `DOCUMENT_EXTENSIONS`: an
130
142
  * extension that the classifier does not map to `document` (e.g. `.html`) would never
131
143
  * reach this budget, so listing it here would be dead and misleading.
132
144
  */
133
145
  const DEFAULT_STRUCTURAL_EXCERPT_CHAR_LIMIT = 6000;
134
- export const DOCUMENT_EXCERPT_CHAR_LIMIT = 200_000;
146
+ /**
147
+ * Capture ceiling (chars) for whole-document prose. Static and model-agnostic —
148
+ * the upper bound for both capture AND the dynamic projection clamp, so the
149
+ * captured excerpt is always a superset of any projection. Set well above the
150
+ * largest projection budget (opus/gpt-5.5 ~1M-token windows derive ~0.5M-char
151
+ * budgets) to leave headroom; the absolute bound is disk, not a model window.
152
+ */
153
+ export const DOCUMENT_CAPTURE_CEILING_CHARS = 5_000_000;
154
+ /**
155
+ * Floor (chars) for the seed-stage document projection budget. The static value
156
+ * used when the model window is unknown (mock realization, provider-only seat, an
157
+ * unregistered model, or a registry entry without context_window_tokens). Equal to
158
+ * the prior fixed document excerpt budget, so a model-unaware run is unchanged.
159
+ */
160
+ export const DOCUMENT_EXCERPT_PROJECTION_FLOOR = 200_000;
161
+ /**
162
+ * Fraction of the model context window the projection budget may consume, leaving
163
+ * margin for prompt instructions, non-excerpt payload, output tokens, and
164
+ * tokenization variance. Window-proportional (not a fixed char margin) so the
165
+ * margin scales with the window (C1).
166
+ */
167
+ const WINDOW_BUDGET_FRACTION = 0.5;
168
+ /**
169
+ * Conservative LOWER bound on chars-per-token used to convert a token window into
170
+ * a char budget. NOT the average (~4 for English): CJK-dense prose tokenizes to
171
+ * far fewer chars per token (≈1 or below), so a low bound keeps the char budget
172
+ * from implying more tokens than the window holds (C1). This is an approximation
173
+ * pending a real tokenizer; the CJK-dense live fixture (P6) validates/calibrates
174
+ * it, and the re-design trigger lowers it if even this conservative budget
175
+ * overflows.
176
+ */
177
+ const CHARS_PER_TOKEN_LB = 1;
178
+ /**
179
+ * Chars reserved from the derived budget for the prompt's instructions, the
180
+ * non-excerpt structural payload, and the model's output allowance — all of which
181
+ * also consume the window alongside the document excerpt (C1).
182
+ */
183
+ const PROMPT_OVERHEAD_RESERVE_CHARS = 50_000;
184
+ // Clamp integrity: the capture ceiling must be >= the projection floor so the
185
+ // derived budget's clamp(raw, FLOOR, CEILING) range is well-formed and the
186
+ // captured excerpt is always a superset of any projection (C4). A module-load
187
+ // assert fails fast on a future mis-edit of these constants.
188
+ if (DOCUMENT_EXCERPT_PROJECTION_FLOOR > DOCUMENT_CAPTURE_CEILING_CHARS) {
189
+ throw new Error("Invalid document excerpt budgets: DOCUMENT_EXCERPT_PROJECTION_FLOOR " +
190
+ `(${DOCUMENT_EXCERPT_PROJECTION_FLOOR}) must be <= ` +
191
+ `DOCUMENT_CAPTURE_CEILING_CHARS (${DOCUMENT_CAPTURE_CEILING_CHARS}).`);
192
+ }
135
193
  const TEXT_READABLE_DOCUMENT_EXTENSIONS = new Set([".md", ".txt", ".adoc"]);
194
+ // M3a — explicit positive allowlist of real source-language extensions that earn whole-file
195
+ // capture/projection. The classifier also maps config/data extensions to `code`
196
+ // (.json/.yaml/.yml/.toml/.xml/.env/.cfg/.conf/.lock); those are deliberately NOT here and
197
+ // default to the bounded structural sample (a quality + cost volume reduction — NOT a
198
+ // secret-exposure closure, which is Track 2). Enumerated, no wildcard, so adding a language is
199
+ // a deliberate edit; fail-safe is bounded for any code extension not listed.
200
+ const CODE_WHOLE_CAPTURE_EXTENSIONS = new Set([
201
+ ".c",
202
+ ".cc",
203
+ ".cpp",
204
+ ".cs",
205
+ ".css",
206
+ ".dockerfile",
207
+ ".go",
208
+ ".graphql",
209
+ ".java",
210
+ ".js",
211
+ ".jsx",
212
+ ".kt",
213
+ ".mjs",
214
+ ".php",
215
+ ".prisma",
216
+ ".proto",
217
+ ".py",
218
+ ".rb",
219
+ ".rs",
220
+ ".scss",
221
+ ".sh",
222
+ ".swift",
223
+ ".ts",
224
+ ".tsx",
225
+ ]);
226
+ // Extensionless build-language source basenames the classifier maps to `code` by name
227
+ // (CODE_BASENAMES). These are real source/build scripts → whole capture. The config/data
228
+ // basenames (package.json/tsconfig.json/cargo.toml/go.mod/pom.xml) are deliberately NOT here
229
+ // — they default to the bounded sample like other config/data (M3a-default), and are small
230
+ // enough that the sample is the whole file anyway.
231
+ const CODE_WHOLE_CAPTURE_BASENAMES = new Set([
232
+ "dockerfile",
233
+ "makefile",
234
+ "rakefile",
235
+ "gemfile",
236
+ ]);
237
+ /**
238
+ * Single whole-capture / full-excerpt eligibility predicate (M3a). Consumed by BOTH the
239
+ * capture owner (`structuralExcerptCharLimit` here) and the seed-stage prompt projection
240
+ * (`isFullExcerptProjectionEligible` in run.ts), so capture and projection can never disagree
241
+ * — a bounded capture under a whole-projection budget would silently author the seed from a
242
+ * partial file. Takes the source REF (path/filename) so it is basename-aware: source-language
243
+ * code (allowlisted extension OR build-language basename) and text-readable documents
244
+ * (.md/.txt/.adoc) earn whole capture; binary documents, config/data code files, and
245
+ * structural-inventory kinds (spreadsheet/database) stay bounded.
246
+ */
247
+ export function isFullExcerptCaptureEligible(kind, ref) {
248
+ const lower = typeof ref === "string" ? ref.toLowerCase() : "";
249
+ const ext = path.extname(lower);
250
+ const base = path.basename(lower);
251
+ if (kind === "code") {
252
+ return CODE_WHOLE_CAPTURE_EXTENSIONS.has(ext) ||
253
+ CODE_WHOLE_CAPTURE_BASENAMES.has(base);
254
+ }
255
+ return kind === "document" && isTextReadableDocumentExtension(ext);
256
+ }
257
+ /**
258
+ * Derives the seed-stage document projection budget (chars) for the active
259
+ * (provider, model) seat from its registered context window. Pure and total
260
+ * (never throws): an unresolved provider/model, an unregistered pair, or an entry
261
+ * without a window all fall back to the static FLOOR, so mock realization and
262
+ * provider-only seats are unchanged.
263
+ *
264
+ * The route key is the MODEL provider (the registry key, e.g. "openai"), not the
265
+ * runtime adapter provider (openai OAuth dispatches as "codex"; anthropic OAuth
266
+ * stays "anthropic"). The caller passes the model provider so the default
267
+ * gpt-5.5 OAuth seat resolves against `openai/gpt-5.5` (see reconstruct-api).
268
+ *
269
+ * This is the SINGLE model→budget conversion point: model literals never reach
270
+ * the char-budget tuning constants, so G2/INV-CFG-1 stay satisfied.
271
+ */
272
+ export function deriveDocumentExcerptProjectionBudget(route, registry) {
273
+ if (!route.provider || !route.modelId) {
274
+ return DOCUMENT_EXCERPT_PROJECTION_FLOOR;
275
+ }
276
+ const window = registry.supported_models.find((entry) => entry.provider === route.provider && entry.model === route.modelId)?.context_window_tokens;
277
+ if (!window)
278
+ return DOCUMENT_EXCERPT_PROJECTION_FLOOR;
279
+ const raw = Math.floor(window * WINDOW_BUDGET_FRACTION * CHARS_PER_TOKEN_LB) -
280
+ PROMPT_OVERHEAD_RESERVE_CHARS;
281
+ return Math.min(Math.max(raw, DOCUMENT_EXCERPT_PROJECTION_FLOOR), DOCUMENT_CAPTURE_CEILING_CHARS);
282
+ }
136
283
  /**
137
284
  * A text-readable document earns the whole-document excerpt budget. Both the capture
138
285
  * (here) and the seed-stage prompt projection (run.ts) consult this single predicate so
@@ -144,8 +291,14 @@ export function isTextReadableDocumentExtension(extension) {
144
291
  TEXT_READABLE_DOCUMENT_EXTENSIONS.has(extension.toLowerCase()));
145
292
  }
146
293
  function structuralExcerptCharLimit(kind, ref) {
147
- if (kind === "document" && isTextReadableDocumentExtension(path.extname(ref))) {
148
- return DOCUMENT_EXCERPT_CHAR_LIMIT;
294
+ // Whole-capture eligibility is the single shared predicate (M3a): a source-language file
295
+ // or a text-readable document is projected whole into seed authoring, so it must be
296
+ // CAPTURED whole too — a small leading sample would make seed authoring read only the
297
+ // file's head while the prompt-projection truncation never fires (capture < projection
298
+ // budget), silently authoring the seed from a partial file. Config/data code files,
299
+ // binary documents, and structural-inventory kinds (spreadsheet/database) stay bounded.
300
+ if (isFullExcerptCaptureEligible(kind, ref)) {
301
+ return DOCUMENT_CAPTURE_CEILING_CHARS;
149
302
  }
150
303
  return DEFAULT_STRUCTURAL_EXCERPT_CHAR_LIMIT;
151
304
  }
@@ -170,7 +323,15 @@ async function textStats(ref, excerptLimit) {
170
323
  };
171
324
  }
172
325
  }
173
- export async function buildReconstructSourceObservation(detection, lineage) {
326
+ export async function buildReconstructSourceObservation(detection, lineage,
327
+ // Defect-3 basis A: the caller asserts whether this ref is the user-provided
328
+ // reconstruct runtime target. Set true ONLY by the initial-target
329
+ // materialization; frontier-discovered / maturation-closure re-entry callers
330
+ // omit it (conservative default false). Every observation literal below sets
331
+ // is_runtime_target_source from this so source-safety can grant the
332
+ // material_claim/public_output tiers by provenance without leaking to
333
+ // non-target sources.
334
+ options) {
174
335
  if (!detection.exists || !isConcreteTargetMaterialKind(detection.kind)) {
175
336
  return null;
176
337
  }
@@ -193,6 +354,17 @@ export async function buildReconstructSourceObservation(detection, lineage) {
193
354
  return null;
194
355
  throw error;
195
356
  }
357
+ // Spreadsheet sources route through the shared deterministic structure
358
+ // observer instead of the generic raw-text path (design S1 §2.2 / §11).
359
+ if (detection.kind === "spreadsheet") {
360
+ return buildSpreadsheetSourceObservation({
361
+ detection,
362
+ stat,
363
+ location,
364
+ lineage,
365
+ isRuntimeTargetSource: Boolean(options?.isRuntimeTargetSource),
366
+ });
367
+ }
196
368
  const stats = stat.isFile()
197
369
  ? await textStats(detection.ref, structuralExcerptCharLimit(detection.kind, detection.ref))
198
370
  : {
@@ -210,6 +382,7 @@ export async function buildReconstructSourceObservation(detection, lineage) {
210
382
  round_id: lineage?.roundId ?? "initial_source_frontier",
211
383
  observation_batch_id: lineage?.observationBatchId ?? "source-observation-batch:initial",
212
384
  triggering_frontier_validation_ref: lineage?.triggeringFrontierValidationRef ?? null,
385
+ is_runtime_target_source: Boolean(options?.isRuntimeTargetSource),
213
386
  target_material_kind: detection.kind,
214
387
  adapter_id: `minimal-${detection.kind}-structure-observer`,
215
388
  source_ref: detection.ref,
@@ -233,6 +406,96 @@ export async function buildReconstructSourceObservation(detection, lineage) {
233
406
  }
234
407
  return observation;
235
408
  }
409
+ /**
410
+ * Observe a spreadsheet source through the shared structure observer (design S1
411
+ * §2.2) — a deterministic, LLM-free structural inventory. The observation carries
412
+ * NO raw cell values: it never emits the generic path's `content_excerpt` (which for
413
+ * a workbook would be raw data values) and the inventory's aggregate-only vocab
414
+ * leaves `top_values` absent. The one NARROWED exception (design-C, not raw data) is
415
+ * `data_validations[].members` — the bounded, DECLARED type=list enum labels parsed from a
416
+ * formula1 literal (never observed values). `content_sha256` is the RAW-byte hash (§11 HASH-1)
417
+ * surfaced at the structural_data top level because the downstream source-scout-pack
418
+ * reads it there; the full inventory is nested under `workbook_inventory` as the
419
+ * structural substrate the seed-authoring prompt observes. xlsx-family kinds are
420
+ * not yet extractable (P4) and arrive here carrying an `unsupported_reason`.
421
+ */
422
+ /**
423
+ * The honest observation summary for a workbook inventory (P6): the base disclosure
424
+ * plus the fixed phrases the boundary gate asserts when the capture/macro flags are
425
+ * set. This is the single emit path; it and the gate bind to the SAME exported phrase
426
+ * consts, so assert + emit cannot drift. Exported for the gate-binding regression test.
427
+ */
428
+ export function buildSpreadsheetObservationSummary(basename, inventory) {
429
+ const parts = [
430
+ inventory.unsupported_reason
431
+ ? `spreadsheet workbook observed at ${basename} — extraction unsupported (${inventory.unsupported_reason}), structure_inspected_only`
432
+ : `spreadsheet workbook observed at ${basename} — ${inventory.sheets.length} sheet(s), structure_inspected_only`,
433
+ ];
434
+ if (inventory.capture_truncated)
435
+ parts.push(SPREADSHEET_CAPTURE_TRUNCATED_PHRASE);
436
+ if (inventory.macro_present)
437
+ parts.push(SPREADSHEET_MACRO_PRESENT_PHRASE);
438
+ return parts.join("; ");
439
+ }
440
+ async function buildSpreadsheetSourceObservation(args) {
441
+ const { detection, stat, location, lineage, isRuntimeTargetSource } = args;
442
+ const basename = path.basename(detection.ref);
443
+ const extension = path.extname(detection.ref).toLowerCase();
444
+ // Route through the single shared projection: the structural_data inventory
445
+ // carries aggregate counts / structure only, never raw cell values (the sole narrowed
446
+ // exception is data_validations[].members — bounded declared type=list enum labels).
447
+ const inventory = projectInventoryForAdmission(await observeSpreadsheetSource(detection.ref));
448
+ const summary = buildSpreadsheetObservationSummary(basename, inventory);
449
+ const observationId = stableObservationId({ sourceRef: detection.ref, location });
450
+ // P1-C1 §5.7: the ComprehensionArtifact COMPANIONS the inventory (joined by observation_id) — a
451
+ // deterministic-only edition in this cut (LLM-touch fields are explicit not_applicable). It is
452
+ // validated by validateSourceObservationBoundary below (construction throws on an invalid contract).
453
+ const comprehensionArtifact = buildDeterministicComprehensionArtifact({
454
+ observationId,
455
+ inventory,
456
+ });
457
+ const observation = {
458
+ observation_id: observationId,
459
+ round_id: lineage?.roundId ?? "initial_source_frontier",
460
+ observation_batch_id: lineage?.observationBatchId ?? "source-observation-batch:initial",
461
+ triggering_frontier_validation_ref: lineage?.triggeringFrontierValidationRef ?? null,
462
+ is_runtime_target_source: isRuntimeTargetSource,
463
+ target_material_kind: "spreadsheet",
464
+ adapter_id: SPREADSHEET_OBSERVER_ADAPTER_ID,
465
+ source_ref: detection.ref,
466
+ location,
467
+ summary,
468
+ structural_data: {
469
+ basename,
470
+ extension: extension || null,
471
+ path_kind: stat.isDirectory() ? "directory" : "file",
472
+ size_bytes: stat.isFile() ? stat.size : null,
473
+ // Raw-byte hash (§11 HASH-1) surfaced top-level for source-scout-pack
474
+ // admission, which reads structural_data.content_sha256.
475
+ content_sha256: inventory.content_sha256,
476
+ workbook_inventory: inventory,
477
+ comprehension_artifact: comprehensionArtifact,
478
+ },
479
+ };
480
+ const validation = validateSourceObservationBoundary(observation);
481
+ if (!validation.valid) {
482
+ throw new Error(`Invalid source observation boundary for ${detection.ref}: ${validation.violations.join("; ")}`);
483
+ }
484
+ return observation;
485
+ }
486
+ /**
487
+ * The `unsupported_reason` of a spreadsheet observation's workbook inventory, or
488
+ * null when the observation is not an unsupported workbook. An unsupported workbook
489
+ * (.xls/.xlsb/.ods, corrupt, oversized) carries no structural evidence, so the
490
+ * materialize loop demotes it to a skip rather than admitting an empty observation.
491
+ */
492
+ export function spreadsheetUnsupportedReason(observation) {
493
+ if (observation.target_material_kind !== "spreadsheet")
494
+ return null;
495
+ const inventory = observation.structural_data.workbook_inventory;
496
+ const reason = inventory?.unsupported_reason;
497
+ return typeof reason === "string" && reason.length > 0 ? reason : null;
498
+ }
236
499
  function stableFrontierRefId(unit) {
237
500
  const digest = crypto
238
501
  .createHash("sha256")
@@ -357,9 +620,32 @@ export async function materializeReconstructPreparationArtifacts(params) {
357
620
  const refDetection = detection.per_ref.find((candidate) => candidate.ref === unit.ref);
358
621
  if (!refDetection)
359
622
  continue;
360
- const observation = await buildReconstructSourceObservation(refDetection);
623
+ // Defect-3 basis A: these are the user-provided reconstruct runtime-target
624
+ // refs (the initial target inventory), so they are runtime-target sources.
625
+ const observation = await buildReconstructSourceObservation(refDetection, undefined, {
626
+ isRuntimeTargetSource: true,
627
+ });
361
628
  if (observation) {
362
- observations.push(observation);
629
+ const unsupportedReason = spreadsheetUnsupportedReason(observation);
630
+ if (unsupportedReason) {
631
+ // A workbook whose format the observer cannot extract (.xls/.xlsb/.ods,
632
+ // corrupt, oversized) yields an inventory carrying only `unsupported_reason`
633
+ // — no structural evidence. After the gate flip such a ref is runnable and
634
+ // produces an observation, so without this demotion a sole legacy-format
635
+ // target would pass the zero-observation halt and reach LLM authoring with
636
+ // empty evidence. Demote it to a skip so the run fails loud as unsupported
637
+ // (the evidence gate stays honest about un-observed material).
638
+ unit.scan_status = "skipped";
639
+ unit.skip_reason = `spreadsheet extraction unsupported: ${unsupportedReason}`;
640
+ skippedRefs.push({
641
+ ref: unit.ref,
642
+ target_material_kind: unit.target_material_kind,
643
+ reason: unit.skip_reason,
644
+ });
645
+ }
646
+ else {
647
+ observations.push(observation);
648
+ }
363
649
  }
364
650
  else {
365
651
  // buildReconstructSourceObservation returns null when the ref is no longer