onto-mcp 0.4.11 → 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 (139) hide show
  1. package/.onto/authority/core-lexicon.yaml +1 -1
  2. package/.onto/authority/supported-models.yaml +153 -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 +202 -49
  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/nesting-batch-worker-contract.md +1 -1
  11. package/.onto/processes/review/productized-live-path.md +9 -5
  12. package/.onto/processes/review/prompt-execution-runner-contract.md +32 -8
  13. package/.onto/processes/review/review-target-profile-contract.md +47 -4
  14. package/.onto/processes/shared/pipeline-execution-ledger-contract.md +11 -0
  15. package/.onto/processes/shared/target-material-kind-contract.md +1 -1
  16. package/README.md +64 -1
  17. package/dist/cli.js +11 -1
  18. package/dist/core-api/reconstruct-api.js +679 -18
  19. package/dist/core-api/review-api.js +70 -5
  20. package/dist/core-api/review-progress.js +7 -0
  21. package/dist/core-api/runtime-observation.js +7 -0
  22. package/dist/core-runtime/artifact-io.js +131 -0
  23. package/dist/core-runtime/cli/claude-code-review-unit-executor.js +21 -9
  24. package/dist/core-runtime/cli/claude-nesting-batch-worker.js +4 -2
  25. package/dist/core-runtime/cli/materialize-review-prompt-packets.js +68 -14
  26. package/dist/core-runtime/cli/prepare-review-session.js +34 -1
  27. package/dist/core-runtime/cli/render-review-final-output.js +2 -1
  28. package/dist/core-runtime/cli/run-review-prompt-execution.js +758 -48
  29. package/dist/core-runtime/cli/run-seats.js +18 -0
  30. package/dist/core-runtime/cli/runtime-submit-context.js +10 -2
  31. package/dist/core-runtime/cli/unit-resubmit.js +269 -0
  32. package/dist/core-runtime/cli/worker-structured-output.js +80 -2
  33. package/dist/core-runtime/discovery/host-detection.js +71 -22
  34. package/dist/core-runtime/discovery/review-cert-assemble.js +208 -0
  35. package/dist/core-runtime/discovery/review-cert-record.js +442 -0
  36. package/dist/core-runtime/discovery/review-execution-units.js +22 -0
  37. package/dist/core-runtime/discovery/seat-inventory.js +167 -0
  38. package/dist/core-runtime/discovery/settings-chain.js +311 -52
  39. package/dist/core-runtime/discovery/supported-models.js +289 -25
  40. package/dist/core-runtime/discovery/synthesize-cert-assemble.js +197 -0
  41. package/dist/core-runtime/discovery/synthesize-cert-capsule.js +399 -0
  42. package/dist/core-runtime/discovery/synthesize-cert-judge.js +340 -0
  43. package/dist/core-runtime/discovery/synthesize-cert-loop.js +234 -0
  44. package/dist/core-runtime/discovery/synthesize-cert-mutation.js +117 -0
  45. package/dist/core-runtime/discovery/synthesize-cert-packet.js +182 -0
  46. package/dist/core-runtime/discovery/synthesize-cert-record.js +831 -0
  47. package/dist/core-runtime/discovery/synthesize-cert-sampler.js +336 -0
  48. package/dist/core-runtime/effort-calibration-ingest.js +326 -0
  49. package/dist/core-runtime/effort-calibration-reconstruct.js +47 -0
  50. package/dist/core-runtime/effort-calibration-review.js +22 -0
  51. package/dist/core-runtime/effort-calibration-sweep.js +110 -0
  52. package/dist/core-runtime/effort-frontier.js +134 -0
  53. package/dist/core-runtime/llm/claude-bin.js +77 -0
  54. package/dist/core-runtime/llm/dispatch-breaker.js +412 -0
  55. package/dist/core-runtime/llm/dispatch-fallback-adapter-capabilities.js +33 -0
  56. package/dist/core-runtime/llm/llm-caller.js +316 -35
  57. package/dist/core-runtime/llm/mock-llm-realization.js +35 -4
  58. package/dist/core-runtime/llm/model-switcher.js +1 -1
  59. package/dist/core-runtime/llm/openai-responses-incomplete-error.js +46 -0
  60. package/dist/core-runtime/llm/sealed-dispatch-capability.js +359 -0
  61. package/dist/core-runtime/llm/structured-dispatch-error.js +15 -0
  62. package/dist/core-runtime/onboard/claude-profile-scan.js +65 -0
  63. package/dist/core-runtime/onboard/cli-host.js +5 -2
  64. package/dist/core-runtime/onboard/configure-provider.js +33 -1
  65. package/dist/core-runtime/onboard/host-target.js +28 -3
  66. package/dist/core-runtime/onboard/register.js +54 -12
  67. package/dist/core-runtime/pipeline-execution-ledger.js +10 -0
  68. package/dist/core-runtime/reconstruct/artifact-types.js +37 -0
  69. package/dist/core-runtime/reconstruct/benchmark-evidence.js +16 -0
  70. package/dist/core-runtime/reconstruct/claim-projection-validation.js +19 -5
  71. package/dist/core-runtime/reconstruct/competency-projection-contract.js +63 -0
  72. package/dist/core-runtime/reconstruct/comprehension-artifact.js +321 -0
  73. package/dist/core-runtime/reconstruct/comprehension-reduce.js +367 -0
  74. package/dist/core-runtime/reconstruct/comprehension-semantic-map.js +771 -0
  75. package/dist/core-runtime/reconstruct/contract-registry.js +20 -0
  76. package/dist/core-runtime/reconstruct/directive-validation.js +4 -3
  77. package/dist/core-runtime/reconstruct/dispatch-fallback-artifacts.js +640 -0
  78. package/dist/core-runtime/reconstruct/execution-telemetry.js +84 -9
  79. package/dist/core-runtime/reconstruct/final-output-sections.js +89 -0
  80. package/dist/core-runtime/reconstruct/governing-snapshot.js +36 -1
  81. package/dist/core-runtime/reconstruct/leaf-reader.js +305 -0
  82. package/dist/core-runtime/reconstruct/llm-dispatch-failure.js +270 -0
  83. package/dist/core-runtime/reconstruct/llm-touch-fingerprint.js +94 -0
  84. package/dist/core-runtime/reconstruct/material-admission-validation.js +20 -5
  85. package/dist/core-runtime/reconstruct/material-profile-validation.js +24 -6
  86. package/dist/core-runtime/reconstruct/materialize-preparation.js +380 -23
  87. package/dist/core-runtime/reconstruct/maturation-validation.js +1522 -67
  88. package/dist/core-runtime/reconstruct/mock-llm-realization.js +120 -1
  89. package/dist/core-runtime/reconstruct/obligation-assertion.js +18 -0
  90. package/dist/core-runtime/reconstruct/ontology-seed-validation.js +39 -7
  91. package/dist/core-runtime/reconstruct/output-budget.js +6 -0
  92. package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +112 -5
  93. package/dist/core-runtime/reconstruct/post-seed-validation.js +122 -11
  94. package/dist/core-runtime/reconstruct/proof-authority-validation.js +2 -6
  95. package/dist/core-runtime/reconstruct/purpose-authority-validation.js +119 -7
  96. package/dist/core-runtime/reconstruct/record.js +49 -3
  97. package/dist/core-runtime/reconstruct/registry-verification-validation.js +41 -5
  98. package/dist/core-runtime/reconstruct/run-control-validation.js +1019 -37
  99. package/dist/core-runtime/reconstruct/run.js +5409 -517
  100. package/dist/core-runtime/reconstruct/seed-authoring-readiness-validation.js +56 -8
  101. package/dist/core-runtime/reconstruct/semantic-quality-gate.js +112 -7
  102. package/dist/core-runtime/reconstruct/source-observation-delta-validation.js +53 -5
  103. package/dist/core-runtime/reconstruct/source-observations.js +166 -0
  104. package/dist/core-runtime/reconstruct/source-safety-validation.js +77 -188
  105. package/dist/core-runtime/reconstruct/source-scout-pack-validation.js +50 -6
  106. package/dist/core-runtime/reconstruct/terminal-validation.js +145 -13
  107. package/dist/core-runtime/review/continuation-plan.js +12 -5
  108. package/dist/core-runtime/review/issue-artifact-runtime.js +4 -1
  109. package/dist/core-runtime/review/materializers.js +100 -11
  110. package/dist/core-runtime/review/nesting-batch.js +6 -2
  111. package/dist/core-runtime/review/obligation-shardability.js +138 -0
  112. package/dist/core-runtime/review/pipeline-execution-ledger.js +42 -2
  113. package/dist/core-runtime/review/review-artifact-utils.js +219 -7
  114. package/dist/core-runtime/review/review-execution-steps.js +32 -8
  115. package/dist/core-runtime/review/review-progress-contract.js +8 -0
  116. package/dist/core-runtime/review/review-prompt-budget.js +97 -0
  117. package/dist/core-runtime/review/semantic-quality-gate.js +33 -7
  118. package/dist/core-runtime/review/spreadsheet-review-disposition.js +94 -0
  119. package/dist/core-runtime/route-identity.js +182 -0
  120. package/dist/core-runtime/spreadsheet-header-escalation.js +204 -0
  121. package/dist/core-runtime/spreadsheet-structure-observer.js +2689 -0
  122. package/dist/core-runtime/target-material-kind.js +58 -0
  123. package/dist/mcp/server.js +27 -5
  124. package/dist/mcp/tool-schemas.js +8 -0
  125. package/dist/tui/app.js +167 -0
  126. package/dist/tui/data/event-follower.js +116 -0
  127. package/dist/tui/data/node-detail.js +42 -0
  128. package/dist/tui/data/projection-poll.js +26 -0
  129. package/dist/tui/data/session-discovery.js +65 -0
  130. package/dist/tui/index.js +108 -0
  131. package/dist/tui/screens/log.js +25 -0
  132. package/dist/tui/screens/node-detail.js +17 -0
  133. package/dist/tui/screens/session-selector.js +26 -0
  134. package/dist/tui/screens/workflow-tree.js +90 -0
  135. package/dist/tui/view-model/reconstruct-adapter.js +162 -0
  136. package/dist/tui/view-model/review-adapter.js +270 -0
  137. package/dist/tui/view-model/tree-view-model.js +19 -0
  138. package/package.json +20 -3
  139. package/settings.example.json +77 -16
@@ -1,9 +1,11 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { stringify as stringifyYaml } from "yaml";
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",
@@ -17,10 +19,6 @@ function isConcreteTargetMaterialKind(kind) {
17
19
  function isoNow() {
18
20
  return new Date().toISOString();
19
21
  }
20
- async function writeYamlDocument(filePath, value) {
21
- await fs.mkdir(path.dirname(filePath), { recursive: true });
22
- await fs.writeFile(filePath, stringifyYaml(value), "utf8");
23
- }
24
22
  function stableObservationId(args) {
25
23
  const digest = crypto
26
24
  .createHash("sha256")
@@ -114,10 +112,199 @@ function defaultProfileForKind(profiles, kind) {
114
112
  return matchingProfiles.find((candidate) => candidate.is_default_for_kind) ??
115
113
  matchingProfiles[0];
116
114
  }
117
- async function textStats(ref) {
115
+ /**
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).
135
+ *
136
+ * Material kind is detected by extension (target-material-kind.ts `DOCUMENT_EXTENSIONS`),
137
+ * and `document` includes binary formats (.pdf/.docx/.ppt/.rtf) that `textStats` still
138
+ * reads as UTF-8. Only text-readable document formats earn the whole-document budget —
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
142
+ * extension that the classifier does not map to `document` (e.g. `.html`) would never
143
+ * reach this budget, so listing it here would be dead and misleading.
144
+ */
145
+ const DEFAULT_STRUCTURAL_EXCERPT_CHAR_LIMIT = 6000;
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
+ }
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
+ }
283
+ /**
284
+ * A text-readable document earns the whole-document excerpt budget. Both the capture
285
+ * (here) and the seed-stage prompt projection (run.ts) consult this single predicate so
286
+ * a binary document (.pdf/.docx/.ppt/.rtf) is never expanded — neither the captured
287
+ * artifact nor the prompt carries decoded binary bytes.
288
+ */
289
+ export function isTextReadableDocumentExtension(extension) {
290
+ return (typeof extension === "string" &&
291
+ TEXT_READABLE_DOCUMENT_EXTENSIONS.has(extension.toLowerCase()));
292
+ }
293
+ function structuralExcerptCharLimit(kind, ref) {
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;
302
+ }
303
+ return DEFAULT_STRUCTURAL_EXCERPT_CHAR_LIMIT;
304
+ }
305
+ async function textStats(ref, excerptLimit) {
118
306
  try {
119
307
  const text = await fs.readFile(ref, "utf8");
120
- const excerptLimit = 6000;
121
308
  return {
122
309
  line_count: text.length === 0 ? 0 : text.split(/\r?\n/).length,
123
310
  char_count: text.length,
@@ -136,20 +323,57 @@ async function textStats(ref) {
136
323
  };
137
324
  }
138
325
  }
139
- 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) {
140
335
  if (!detection.exists || !isConcreteTargetMaterialKind(detection.kind)) {
141
336
  return null;
142
337
  }
143
338
  const extension = path.extname(detection.ref).toLowerCase();
144
339
  const location = detection.ref;
145
- const stat = await fs.stat(detection.ref);
146
- const stats = stat.isFile() ? await textStats(detection.ref) : {
147
- line_count: null,
148
- char_count: null,
149
- content_sha256: null,
150
- content_excerpt: null,
151
- excerpt_truncated: false,
152
- };
340
+ // Re-observation runs after an earlier detection: the ref may have vanished
341
+ // in between (TOCTOU). Treat a missing ref as nothing-to-observe (degrade to
342
+ // null, like the !detection.exists guard above) instead of crashing the run
343
+ // with an uncontextualized ENOENT. Other stat failures still propagate.
344
+ let stat;
345
+ try {
346
+ stat = await fs.stat(detection.ref);
347
+ }
348
+ catch (error) {
349
+ const code = error.code;
350
+ // ENOENT: the ref was deleted; ENOTDIR: a parent path component became a
351
+ // file. Both mean the ref is no longer an observable source (TOCTOU), so
352
+ // degrade to null. Other stat failures still propagate.
353
+ if (code === "ENOENT" || code === "ENOTDIR")
354
+ return null;
355
+ throw error;
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
+ }
368
+ const stats = stat.isFile()
369
+ ? await textStats(detection.ref, structuralExcerptCharLimit(detection.kind, detection.ref))
370
+ : {
371
+ line_count: null,
372
+ char_count: null,
373
+ content_sha256: null,
374
+ content_excerpt: null,
375
+ excerpt_truncated: false,
376
+ };
153
377
  const observation = {
154
378
  observation_id: stableObservationId({
155
379
  sourceRef: detection.ref,
@@ -158,6 +382,7 @@ export async function buildReconstructSourceObservation(detection, lineage) {
158
382
  round_id: lineage?.roundId ?? "initial_source_frontier",
159
383
  observation_batch_id: lineage?.observationBatchId ?? "source-observation-batch:initial",
160
384
  triggering_frontier_validation_ref: lineage?.triggeringFrontierValidationRef ?? null,
385
+ is_runtime_target_source: Boolean(options?.isRuntimeTargetSource),
161
386
  target_material_kind: detection.kind,
162
387
  adapter_id: `minimal-${detection.kind}-structure-observer`,
163
388
  source_ref: detection.ref,
@@ -181,6 +406,96 @@ export async function buildReconstructSourceObservation(detection, lineage) {
181
406
  }
182
407
  return observation;
183
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
+ }
184
499
  function stableFrontierRefId(unit) {
185
500
  const digest = crypto
186
501
  .createHash("sha256")
@@ -291,10 +606,6 @@ export async function materializeReconstructPreparationArtifacts(params) {
291
606
  source: "binding",
292
607
  },
293
608
  };
294
- const initialSourceFrontier = buildInitialSourceFrontier({
295
- sessionId,
296
- inventory,
297
- });
298
609
  const observations = [];
299
610
  const skippedRefs = [];
300
611
  for (const unit of inventory.inventory_units) {
@@ -309,10 +620,56 @@ export async function materializeReconstructPreparationArtifacts(params) {
309
620
  const refDetection = detection.per_ref.find((candidate) => candidate.ref === unit.ref);
310
621
  if (!refDetection)
311
622
  continue;
312
- const observation = await buildReconstructSourceObservation(refDetection);
313
- if (observation)
314
- observations.push(observation);
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
+ });
628
+ if (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
+ }
649
+ }
650
+ else {
651
+ // buildReconstructSourceObservation returns null when the ref is no longer
652
+ // a concrete, existing source at observation time (e.g. deleted between
653
+ // detection and re-observation). Mark the inventory unit skipped — the
654
+ // single source of truth that the initial frontier (built below), the
655
+ // zero-observation halt, and later frontier admission all derive from — so
656
+ // the vanished ref is excluded everywhere instead of being silently dropped
657
+ // or re-queued by the deterministic first-frontier scout.
658
+ unit.scan_status = "skipped";
659
+ unit.skip_reason = "source ref unavailable at observation time";
660
+ skippedRefs.push({
661
+ ref: unit.ref,
662
+ target_material_kind: unit.target_material_kind,
663
+ reason: unit.skip_reason,
664
+ });
665
+ }
315
666
  }
667
+ // Built after observation so refs marked skipped above (vanished mid-run) are
668
+ // excluded from frontier source_refs rather than re-admitted later.
669
+ const initialSourceFrontier = buildInitialSourceFrontier({
670
+ sessionId,
671
+ inventory,
672
+ });
316
673
  const sourceObservations = {
317
674
  schema_version: "1",
318
675
  session_id: sessionId,