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,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Log screen — a tail of a session's `runtime-events.ndjson` (fed by the
4
+ * event-follower in the app). Pure presentation: shows the most recent events
5
+ * with timestamp · source · stream · message. Read-only.
6
+ */
7
+ import { Box, Text } from "ink";
8
+ function clockTime(timestamp) {
9
+ // Render only HH:MM:SS from an ISO timestamp without constructing a Date
10
+ // (avoids locale/clock surprises and keeps it test-stable).
11
+ const match = /T(\d{2}:\d{2}:\d{2})/.exec(timestamp);
12
+ return match ? match[1] : timestamp.slice(0, 8);
13
+ }
14
+ const STREAM_COLOR = {
15
+ model_call: "magenta",
16
+ status: undefined,
17
+ chunk: "gray",
18
+ };
19
+ export function LogScreen({ sessionId, events, maxLines = 18, }) {
20
+ const tail = events.slice(-maxLines);
21
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: `onto watch · ${sessionId} · log` }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tail.length === 0 ? (_jsx(Text, { dimColor: true, children: "no events yet" })) : (tail.map((event, index) => {
22
+ const sourceColor = STREAM_COLOR[event.stream];
23
+ return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: `${clockTime(event.timestamp)} ` }), _jsx(Text, { ...(sourceColor ? { color: sourceColor } : {}), children: `${(event.source.label ?? event.source.kind).padEnd(16)} ` }), _jsx(Text, { children: event.message })] }, index));
24
+ })) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "[tab] tree [q]uit" }) })] }));
25
+ }
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Drill-down detail pane for the node selected in the WorkflowTree HUD — shows
4
+ * the node's status/owner/attempts/failure/output and a tail of its output (the
5
+ * running log / authored artifact). Pure presentation.
6
+ */
7
+ import { Box, Text } from "ink";
8
+ export function NodeDetail({ node, tail }) {
9
+ const meta = [`status ${node.status}`];
10
+ if (node.owner)
11
+ meta.push(`owner ${node.owner}`);
12
+ if (node.attempts && node.attempts > 1)
13
+ meta.push(`try${node.attempts}`);
14
+ if (node.signalAgeSec != null)
15
+ meta.push(`${node.signalAgeSec}s`);
16
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, children: node.label }), _jsx(Text, { dimColor: true, children: meta.join(" · ") }), node.failureMessage ? _jsx(Text, { color: "red", children: node.failureMessage }) : null, node.outputPath ? _jsx(Text, { dimColor: true, children: node.outputPath }) : null, tail.length > 0 ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "\u2500\u2500 output (tail) \u2500\u2500" }), tail.map((line, index) => (_jsx(Text, { children: line }, index)))] })) : null] }));
17
+ }
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * SessionSelector — the `onto watch` entry screen when no session is given (or
4
+ * `s` from a session view). Lists review/reconstruct sessions (most recent
5
+ * first) with a cursor. Pure presentation: selection state is owned by the app.
6
+ */
7
+ import { Box, Text } from "ink";
8
+ function relativeAge(modifiedMs, nowMs) {
9
+ if (!modifiedMs)
10
+ return "";
11
+ const sec = Math.max(0, Math.round((nowMs - modifiedMs) / 1000));
12
+ if (sec < 60)
13
+ return `${sec}s ago`;
14
+ if (sec < 3600)
15
+ return `${Math.round(sec / 60)}m ago`;
16
+ if (sec < 86400)
17
+ return `${Math.round(sec / 3600)}h ago`;
18
+ return `${Math.round(sec / 86400)}d ago`;
19
+ }
20
+ export function SessionSelector({ sessions, selectedIndex, nowMs, }) {
21
+ const now = nowMs ?? Date.now();
22
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "onto watch \u00B7 sessions" }), sessions.length === 0 ? (_jsx(Text, { dimColor: true, children: "no review/reconstruct sessions found" })) : (_jsx(Box, { flexDirection: "column", marginTop: 1, children: sessions.map((session, index) => {
23
+ const active = index === selectedIndex;
24
+ return (_jsxs(Box, { children: [_jsx(Text, { ...(active ? { color: "cyan" } : {}), children: `${active ? "›" : " "} ${session.pipeline.padEnd(11)} ` }), _jsx(Text, { bold: active, children: session.sessionId }), _jsx(Text, { dimColor: true, children: ` ${relativeAge(session.modifiedMs, now)}` })] }, `${session.pipeline}:${session.sessionRoot}`));
25
+ }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "[\u2191\u2193] select [enter] watch [q]uit" }) })] }));
26
+ }
@@ -0,0 +1,90 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * WorkflowTree HUD — the primary `onto watch` screen. Pure presentation: renders
4
+ * a {@link TreeViewModel} (header · narrator · phase-grouped unit tree · footer)
5
+ * for either pipeline. The HUD never branches on pipeline; the adapters already
6
+ * normalized the difference into the view model.
7
+ */
8
+ import { Box, Text } from "ink";
9
+ const NODE_ICON = {
10
+ completed: "✓",
11
+ running: "◐",
12
+ pending: "○",
13
+ failed: "✗",
14
+ halted: "◑",
15
+ skipped: "⊘",
16
+ };
17
+ const NODE_COLOR = {
18
+ completed: "green",
19
+ running: "cyan",
20
+ pending: "gray",
21
+ failed: "red",
22
+ halted: "yellow",
23
+ skipped: "gray",
24
+ };
25
+ const STATUS_BADGE = {
26
+ running: { label: "◐ running", color: "cyan" },
27
+ completed: { label: "✓ completed", color: "green" },
28
+ halted: { label: "◑ halted", color: "yellow" },
29
+ failed: { label: "✗ failed", color: "red" },
30
+ };
31
+ const SEVERITY_ORDER = ["blocker", "high", "medium", "low", "info"];
32
+ const SEVERITY_ABBR = {
33
+ blocker: "blk",
34
+ high: "high",
35
+ medium: "med",
36
+ low: "low",
37
+ info: "info",
38
+ };
39
+ function NodeBadge({ node }) {
40
+ const parts = [];
41
+ if (node.owner)
42
+ parts.push(node.owner);
43
+ if (node.signalAgeSec != null)
44
+ parts.push(`${node.signalAgeSec}s`);
45
+ if (node.attempts && node.attempts > 1)
46
+ parts.push(`try${node.attempts}`);
47
+ if (node.failureMessage)
48
+ parts.push(node.failureMessage);
49
+ if (parts.length === 0)
50
+ return null;
51
+ return _jsx(Text, { dimColor: true, children: ` ${parts.join(" · ")}` });
52
+ }
53
+ /**
54
+ * Surfaces the run-control availability the status projection carries (observe-
55
+ * only: it reports what the operator can do via the MCP/CLI, the HUD never acts).
56
+ * Most relevant for halted/stale sessions where continue is the next step.
57
+ */
58
+ function RunControlRow({ control }) {
59
+ const available = [];
60
+ if (control.continuable)
61
+ available.push("continue");
62
+ if (control.advanceable)
63
+ available.push("advance");
64
+ if (control.cancellable)
65
+ available.push("cancel");
66
+ if (available.length === 0)
67
+ return null;
68
+ return _jsx(Text, { dimColor: true, children: `Controls ${available.join(" · ")} available` });
69
+ }
70
+ function Footer({ vm }) {
71
+ const { findings, counts } = vm.summary;
72
+ if (findings) {
73
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["Findings ", SEVERITY_ORDER.map((s) => `⬤${findings[s]} ${SEVERITY_ABBR[s]}`).join(" ")] }), findings.material.map((title, index) => (_jsx(Text, { dimColor: true, children: ` › ${title}` }, index)))] }));
74
+ }
75
+ if (counts) {
76
+ const pairs = Object.entries(counts)
77
+ .filter(([, value]) => value != null)
78
+ .map(([key, value]) => `${key} ${value}`);
79
+ return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Coverage ", pairs.join(" · ")] }) }));
80
+ }
81
+ return _jsx(Box, { marginTop: 1 });
82
+ }
83
+ export function WorkflowTree({ vm, selectedNodeId }) {
84
+ const badge = STATUS_BADGE[vm.status];
85
+ const phasesDone = vm.phases.filter((p) => p.state === "completed").length;
86
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "onto watch" }), _jsx(Text, { children: ` · ${vm.pipeline} · ${vm.sessionId} ` }), _jsx(Text, { color: badge.color, children: badge.label })] }), vm.headline ? _jsx(Text, { dimColor: true, children: vm.headline }) : null, _jsx(Text, { children: `▸ ${vm.narrator}` }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: `Pipeline ${phasesDone}/${vm.phases.length} phases` }), vm.liveness.secondsSinceSignal != null ? (_jsx(Text, { dimColor: true, children: ` · ${vm.liveness.secondsSinceSignal}s since signal` })) : null] }), _jsx(RunControlRow, { control: vm.runControl }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: vm.phases.map((phase) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: NODE_COLOR[phase.state], children: `${NODE_ICON[phase.state]} ${phase.label}` }), phase.nodes.map((node) => {
87
+ const selected = node.id === selectedNodeId;
88
+ return (_jsxs(Box, { children: [_jsx(Text, { color: NODE_COLOR[node.status], children: `${selected ? "›" : " "} ${NODE_ICON[node.status]} ` }), _jsx(Text, { bold: selected, children: node.label }), _jsx(NodeBadge, { node: node })] }, node.id));
89
+ })] }, phase.id))) }), _jsx(Footer, { vm: vm }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "[q]uit [r]efresh" }) })] }));
90
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * v1 grouping: a single "pipeline" phase holds every reconstruct stage as a
3
+ * node, in canonical stage order (the projection already lists stages in
4
+ * `RECONSTRUCT_STAGE_IDS` order). This keeps the adapter simple and mirrors the
5
+ * design's "stages are the nodes" framing; finer maturation-stage grouping can
6
+ * be layered on later without changing the node mapping.
7
+ */
8
+ const PIPELINE_PHASE_ID = "pipeline";
9
+ const PIPELINE_PHASE_LABEL = "reconstruct pipeline";
10
+ /**
11
+ * Map a reconstruct stage `state` to the shared {@link NodeState}. Reconstruct
12
+ * stage states are a strict subset (`pending | completed | skipped | halted`) —
13
+ * there is no per-stage `running` signal in the projection, so the in-flight
14
+ * stage is surfaced at the phase/headline level (see {@link derivePhaseState}),
15
+ * not on individual nodes.
16
+ */
17
+ function stageStateToNodeState(state) {
18
+ switch (state) {
19
+ case "completed":
20
+ return "completed";
21
+ case "skipped":
22
+ return "skipped";
23
+ case "halted":
24
+ return "halted";
25
+ case "pending":
26
+ return "pending";
27
+ }
28
+ }
29
+ /** Human-ize a snake_case stageId for display, e.g. `ontology_seed` → `ontology seed`. */
30
+ function humanizeStageId(stageId) {
31
+ return stageId.replace(/_/g, " ");
32
+ }
33
+ function stageToNode(stage, hiddenArtifactRef) {
34
+ const outputPath = stage.artifactRefs.find((ref) => ref !== hiddenArtifactRef) ?? null;
35
+ return {
36
+ id: stage.stageId,
37
+ label: humanizeStageId(stage.stageId),
38
+ status: stageStateToNodeState(stage.state),
39
+ // reconstruct nodes badge by stageId (kind) + carry the owner column.
40
+ kind: stage.stageId,
41
+ // owner is optional under exactOptionalPropertyTypes: omit when absent.
42
+ ...(stage.owner ? { owner: stage.owner } : {}),
43
+ // No per-stage live signal in the projection.
44
+ signalAgeSec: null,
45
+ // `reason` is the stage's terminal explanation (skip/halt rationale).
46
+ failureMessage: stage.reason,
47
+ // First authored artifact ref is the drill-down target.
48
+ outputPath,
49
+ };
50
+ }
51
+ /**
52
+ * Phase state for the single pipeline phase. A halted stage halts the phase;
53
+ * otherwise a completed run completes the phase; otherwise it is running (the
54
+ * pipeline is mid-flight regardless of which individual stages are still
55
+ * pending).
56
+ */
57
+ function derivePhaseState(stages, workflowStatus) {
58
+ if (stages.some((stage) => stage.state === "halted"))
59
+ return "halted";
60
+ if (workflowStatus === "completed")
61
+ return "completed";
62
+ return "running";
63
+ }
64
+ /**
65
+ * Map record_stage + stage states to the shared {@link WorkflowStatus}.
66
+ * reconstruct has no `failed` record_stage and no per-stage `running`; we treat
67
+ * a halted stage as a halt (terminal-ish, surfaced for the operator) and a
68
+ * completed record as completed. Everything else is an in-flight run.
69
+ */
70
+ function deriveWorkflowStatus(status) {
71
+ if (status.status === "failed")
72
+ return "failed";
73
+ if (status.status === "completed")
74
+ return "completed";
75
+ // A graceful terminal (design §16.7) is terminal but not completed: surface it as `halted` so
76
+ // the watch loop stops polling and the operator sees an honest stop, not a "session completed".
77
+ if (status.status === "blocked" || status.status === "limited")
78
+ return "halted";
79
+ if (status.progress.stages.some((stage) => stage.state === "halted")) {
80
+ return "halted";
81
+ }
82
+ return "running";
83
+ }
84
+ /** Flatten countSummary + answerabilitySummary into the footer label→value map. */
85
+ function deriveCounts(progress) {
86
+ const c = progress.countSummary;
87
+ const a = progress.answerabilitySummary;
88
+ return {
89
+ observations: c.selectedObservationCount ?? c.sourceObservationCount,
90
+ claims: c.semanticClaimCount,
91
+ confirmed: c.confirmedClaimCount,
92
+ deferredClaims: c.deferredClaimCount,
93
+ CQ: c.competencyQuestionCount,
94
+ passRate: c.passRate,
95
+ // Answerability (maturation) summary — null until metrics are computed.
96
+ declared: a ? a.declaredQuestionCount : null,
97
+ supported: a ? a.supportedQuestionCount : null,
98
+ deferred: a ? a.deferredQuestionCount : null,
99
+ unsupported: a ? a.unsupportedQuestionCount : null,
100
+ };
101
+ }
102
+ /** One-line narrator from the current stage + the most informative counts. */
103
+ function deriveNarrator(progress, workflowStatus) {
104
+ const stage = humanizeStageId(progress.currentStageId);
105
+ const a = progress.answerabilitySummary;
106
+ const lead = workflowStatus === "completed"
107
+ ? "reconstruct complete"
108
+ : workflowStatus === "failed"
109
+ ? `reconstruct failed at ${stage}`
110
+ : workflowStatus === "halted"
111
+ ? `halted at ${stage}`
112
+ : `reconstructing — ${stage}`;
113
+ if (a) {
114
+ return `${lead} · CQ ${a.declaredQuestionCount} · supported ${a.supportedQuestionCount} · deferred ${a.deferredQuestionCount} · unsupported ${a.unsupportedQuestionCount}`;
115
+ }
116
+ const claims = progress.countSummary.semanticClaimCount;
117
+ return claims === null ? lead : `${lead} · ${claims} claims`;
118
+ }
119
+ /**
120
+ * Pure projection: {@link ReconstructSessionStatus} → {@link TreeViewModel}.
121
+ *
122
+ * `sessionRoot` is threaded explicitly (the caller already knows it) so the view
123
+ * model header is independent of how the status was fetched.
124
+ */
125
+ export function reconstructStatusToTreeViewModel(status, sessionRoot) {
126
+ const { progress } = status;
127
+ const workflowStatus = deriveWorkflowStatus(status);
128
+ const hiddenArtifactRef = status.status === "failed"
129
+ ? status.failure.failure_artifact_ref
130
+ : null;
131
+ const nodes = progress.stages.map((stage) => stageToNode(stage, hiddenArtifactRef));
132
+ const phase = {
133
+ id: PIPELINE_PHASE_ID,
134
+ label: PIPELINE_PHASE_LABEL,
135
+ state: derivePhaseState(progress.stages, workflowStatus),
136
+ nodes,
137
+ };
138
+ return {
139
+ pipeline: "reconstruct",
140
+ sessionId: status.sessionId,
141
+ sessionRoot,
142
+ status: workflowStatus,
143
+ narrator: deriveNarrator(progress, workflowStatus),
144
+ liveness: {
145
+ state: progress.liveness.state,
146
+ // The projection carries no per-signal timestamp for reconstruct.
147
+ secondsSinceSignal: null,
148
+ pollMs: progress.liveness.recommendedPollIntervalMs,
149
+ },
150
+ phases: [phase],
151
+ summary: { counts: deriveCounts(progress) },
152
+ // reconstruct status carries no rich run_control (unlike review): be
153
+ // conservative — only a still-running session is cancellable; continue /
154
+ // advance are display-only false until a control path exists (design §4,
155
+ // later stage E).
156
+ runControl: {
157
+ cancellable: workflowStatus === "running",
158
+ continuable: false,
159
+ advanceable: false,
160
+ },
161
+ };
162
+ }
@@ -0,0 +1,270 @@
1
+ import { REVIEW_PROGRESS_STEPS, } from "../../core-api/review-progress.js";
2
+ /** Phase that collects units the projection has not bound to a progress step. */
3
+ const UNCATEGORIZED_PHASE_ID = "unassigned";
4
+ const UNCATEGORIZED_PHASE_LABEL = "unassigned units";
5
+ /** Canonical progress-step ids — used to reject non-step `completed_steps` keys. */
6
+ const VALID_PROGRESS_STEP_IDS = new Set(REVIEW_PROGRESS_STEPS.map((step) => step.id));
7
+ function isRecord(value) {
8
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9
+ }
10
+ /** Narrow the `unknown` presentation payload to the read-only fields we use. */
11
+ function readProgressPresentationInput(status) {
12
+ const input = status.llmPresentation?.progress?.input;
13
+ return isRecord(input) ? input : {};
14
+ }
15
+ /**
16
+ * Map a runtime unit status to the shared {@link NodeState}. `retrying` and
17
+ * `running_stale` are both still in-flight (the HUD surfaces the retry count /
18
+ * stale signal age separately), so they collapse to `running`.
19
+ */
20
+ function unitStatusToNodeState(unitStatus) {
21
+ switch (unitStatus) {
22
+ case "pending":
23
+ return "pending";
24
+ case "running":
25
+ case "retrying":
26
+ case "running_stale":
27
+ return "running";
28
+ case "completed":
29
+ return "completed";
30
+ case "failed":
31
+ return "failed";
32
+ }
33
+ }
34
+ function unitToNode(unit) {
35
+ return {
36
+ id: unit.unitId,
37
+ label: unit.publicAlias,
38
+ status: unitStatusToNodeState(unit.status),
39
+ kind: unit.unitKind,
40
+ signalAgeSec: unit.secondsSinceLatestSignal,
41
+ attempts: unit.attemptCount,
42
+ failureMessage: unit.failureMessage,
43
+ outputPath: drillDownPath(unit),
44
+ };
45
+ }
46
+ /**
47
+ * Drill-down target for a unit. A completed unit's authored output is the
48
+ * authoritative artifact. For an active/failed unit that planned output may not
49
+ * exist yet, so prefer the live running-log (the projection only sets
50
+ * `runningLogRef` when that file is present) and fall back to the planned output.
51
+ */
52
+ function drillDownPath(unit) {
53
+ if (unit.status === "completed") {
54
+ return unit.outputPath ?? unit.runningLogRef ?? null;
55
+ }
56
+ return unit.runningLogRef ?? unit.outputPath ?? null;
57
+ }
58
+ /**
59
+ * State of the current (in-flight) progress step, given the workflow's own
60
+ * status: a halted or failed run is terminal, and its current step is the
61
+ * halt/failure locus — surface that rather than a misleading "running". A live
62
+ * run's current step is running.
63
+ */
64
+ function currentStepState(workflowStatus) {
65
+ if (workflowStatus === "failed")
66
+ return "failed";
67
+ if (workflowStatus === "halted")
68
+ return "halted";
69
+ return "running";
70
+ }
71
+ /**
72
+ * Phase state from its nodes (+ the projection's completed step ids). A failed
73
+ * node fails the phase; any in-flight node makes it running; a step listed in
74
+ * `completed_steps` (or all nodes completed) completes it; a halted node halts
75
+ * it. A step at the current step whose units exist but have not signalled yet
76
+ * takes the workflow's current-step state — matching {@link emptyStepState}'s
77
+ * current-step branch so the same current step reports the same state whether or
78
+ * not it carries units. Otherwise it is pending.
79
+ */
80
+ function derivePhaseState(nodes, stepId, completedStepIds, stepNumber, currentStep, workflowStatus) {
81
+ if (nodes.some((node) => node.status === "failed"))
82
+ return "failed";
83
+ if (nodes.some((node) => node.status === "running"))
84
+ return "running";
85
+ if (nodes.some((node) => node.status === "halted"))
86
+ return "halted";
87
+ if (stepId !== null && completedStepIds.has(stepId))
88
+ return "completed";
89
+ if (nodes.length > 0 && nodes.every((node) => node.status === "completed")) {
90
+ return "completed";
91
+ }
92
+ if (nodes.length > 0 && nodes.every((node) => node.status === "skipped")) {
93
+ return "skipped";
94
+ }
95
+ if (stepNumber != null && currentStep != null && stepNumber === currentStep) {
96
+ return currentStepState(workflowStatus);
97
+ }
98
+ return "pending";
99
+ }
100
+ /**
101
+ * State for a progress step that has no bound units: completed when the workflow
102
+ * itself completed, when the step is in `completed_steps`, or when it sits before
103
+ * the current step; the workflow's current-step state at the current step (so a
104
+ * halted/failed run points at its halt/failure locus, not a phantom "running");
105
+ * otherwise pending. This keeps idle/completed steps visible so the HUD shows the
106
+ * whole pipeline (not only the steps that happened to carry units).
107
+ */
108
+ function emptyStepState(stepId, stepNumber, completedStepIds, currentStep, workflowStatus) {
109
+ if (workflowStatus === "completed")
110
+ return "completed";
111
+ if (completedStepIds.has(stepId))
112
+ return "completed";
113
+ if (currentStep != null) {
114
+ if (stepNumber < currentStep)
115
+ return "completed";
116
+ if (stepNumber === currentStep)
117
+ return currentStepState(workflowStatus);
118
+ }
119
+ return "pending";
120
+ }
121
+ /**
122
+ * Emit a phase for EVERY progress step (in canonical order), attaching any units
123
+ * bound to it, so completed/idle steps stay visible. Units the projection left
124
+ * unbound (`progressStepId === null`) collect into a trailing uncategorized phase.
125
+ */
126
+ function derivePhases(status, completedStepIds, currentStep, workflowStatus) {
127
+ const byStep = new Map();
128
+ for (const unit of status.unitProgress ?? []) {
129
+ const key = unit.progressStepId;
130
+ const bucket = byStep.get(key);
131
+ if (bucket)
132
+ bucket.push(unitToNode(unit));
133
+ else
134
+ byStep.set(key, [unitToNode(unit)]);
135
+ }
136
+ const phases = REVIEW_PROGRESS_STEPS.map((spec) => {
137
+ const nodes = byStep.get(spec.id) ?? [];
138
+ const state = nodes.length > 0
139
+ ? derivePhaseState(nodes, spec.id, completedStepIds, spec.step, currentStep, workflowStatus)
140
+ : emptyStepState(spec.id, spec.step, completedStepIds, currentStep, workflowStatus);
141
+ return { id: spec.id, label: spec.label, state, nodes };
142
+ });
143
+ const unassigned = byStep.get(null);
144
+ if (unassigned && unassigned.length > 0) {
145
+ phases.push({
146
+ id: UNCATEGORIZED_PHASE_ID,
147
+ label: UNCATEGORIZED_PHASE_LABEL,
148
+ state: derivePhaseState(unassigned, null, completedStepIds, null, currentStep, workflowStatus),
149
+ nodes: unassigned,
150
+ });
151
+ }
152
+ return phases;
153
+ }
154
+ /**
155
+ * Map the review status union to the shared {@link WorkflowStatus}. A degraded
156
+ * completion is still a completion; `halted_partial` is the operator-facing halt;
157
+ * `prepared` / `unknown` are pre-run states the HUD shows as still-running (the
158
+ * narrator/liveness convey the finer state).
159
+ */
160
+ function deriveWorkflowStatus(status) {
161
+ switch (status) {
162
+ case "completed":
163
+ case "completed_with_degradation":
164
+ return "completed";
165
+ case "halted_partial":
166
+ return "halted";
167
+ case "failed":
168
+ return "failed";
169
+ case "running":
170
+ case "prepared":
171
+ case "unknown":
172
+ return "running";
173
+ }
174
+ }
175
+ /**
176
+ * Run control from the review projection. `cancellationAvailable` /
177
+ * `continuationAvailable` are the authoritative flags; review has no per-round
178
+ * advance, so `advanceable` is left undefined.
179
+ */
180
+ function deriveRunControl(runControl) {
181
+ return {
182
+ cancellable: runControl?.cancellationAvailable ?? false,
183
+ continuable: runControl?.continuationAvailable ?? false,
184
+ };
185
+ }
186
+ const EMPTY_SEVERITY_COUNTS = {
187
+ blocker: 0,
188
+ high: 0,
189
+ medium: 0,
190
+ low: 0,
191
+ info: 0,
192
+ };
193
+ /**
194
+ * Footer findings: severity counts + material issue titles. Works for both the
195
+ * full and compact result-classification projections — material titles come from
196
+ * `material_issues[]` (full) or `material_issue_signals[]` (compact).
197
+ */
198
+ function deriveFindings(summary) {
199
+ if (!summary) {
200
+ return { ...EMPTY_SEVERITY_COUNTS, material: [] };
201
+ }
202
+ const counts = {
203
+ blocker: summary.severity_counts.blocker,
204
+ high: summary.severity_counts.high,
205
+ medium: summary.severity_counts.medium,
206
+ low: summary.severity_counts.low,
207
+ info: summary.severity_counts.info,
208
+ };
209
+ return { ...counts, material: materialIssueTitles(summary) };
210
+ }
211
+ function isCompactSummary(summary) {
212
+ return "material_issue_signals" in summary;
213
+ }
214
+ function materialIssueTitles(summary) {
215
+ if (isCompactSummary(summary)) {
216
+ return summary.material_issue_signals.map((signal) => titleLine(signal.issue_id, signal.signal));
217
+ }
218
+ return summary.material_issues.map((issue) => titleLine(issue.issue_id, issue.problem_definition ?? issue.issue_statement ?? issue.impact));
219
+ }
220
+ function titleLine(issueId, text) {
221
+ const trimmed = (text ?? "").trim();
222
+ return trimmed.length > 0 ? `${issueId}: ${trimmed}` : issueId;
223
+ }
224
+ /**
225
+ * Narrator line: prefer the latest progress signal summary, fall back to the
226
+ * liveness summary, then to the bare status. Mirrors the design's
227
+ * `latest_update.summary / liveness.summary` rule.
228
+ */
229
+ function deriveNarrator(input, workflowStatus) {
230
+ const latest = input.latest_update?.summary?.trim();
231
+ if (latest)
232
+ return latest;
233
+ const liveness = input.liveness?.summary?.trim();
234
+ if (liveness)
235
+ return liveness;
236
+ return `review ${workflowStatus}`;
237
+ }
238
+ /**
239
+ * Pure projection: {@link ReviewStatus} → {@link TreeViewModel}.
240
+ *
241
+ * `sessionRoot` is threaded explicitly (the caller already knows it) so the view
242
+ * model header is independent of how the status was fetched.
243
+ */
244
+ export function reviewStatusToTreeViewModel(status, sessionRoot) {
245
+ const input = readProgressPresentationInput(status);
246
+ const workflowStatus = deriveWorkflowStatus(status.status);
247
+ // Consume only real progress-step ids: a prepared session fills
248
+ // `completed_steps` with preparation-artifact keys (interpretation/binding/
249
+ // execution_plan), a different vocabulary, which must not be matched against
250
+ // (or mistaken for) progress-step ids during phase rollup.
251
+ const completedStepIds = new Set((input.progress?.completed_steps ?? []).filter((id) => VALID_PROGRESS_STEP_IDS.has(id)));
252
+ const phases = derivePhases(status, completedStepIds, input.progress?.current_step ?? null, workflowStatus);
253
+ return {
254
+ pipeline: "review",
255
+ sessionId: status.sessionId,
256
+ sessionRoot,
257
+ status: workflowStatus,
258
+ narrator: deriveNarrator(input, workflowStatus),
259
+ liveness: {
260
+ state: input.liveness?.state ?? "unknown",
261
+ secondsSinceSignal: input.liveness?.seconds_since_last_observed_artifact ?? null,
262
+ pollMs: input.liveness?.poll_after_seconds == null
263
+ ? null
264
+ : input.liveness.poll_after_seconds * 1000,
265
+ },
266
+ phases,
267
+ summary: { findings: deriveFindings(input.result_classification_summary) },
268
+ runControl: deriveRunControl(status.runControl),
269
+ };
270
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Pipeline-agnostic view model rendered by the `onto watch` HUD.
3
+ *
4
+ * review and reconstruct expose progress in different shapes (review = per-UNIT
5
+ * live signal; reconstruct = per-STAGE state + owner + domain counts). The
6
+ * review/reconstruct adapters normalize both into this single shape, so the Ink
7
+ * HUD never branches on pipeline. This is a pure derived VIEW — it carries no
8
+ * authority and is never persisted.
9
+ */
10
+ /**
11
+ * True when the workflow has reached a terminal state (stop polling). `halted`
12
+ * is terminal for the watch loop too: it sits paused awaiting an explicit
13
+ * operator continuation that may be a long way off, so the live loop stops
14
+ * rather than re-reading an unchanging projection every interval. The operator
15
+ * re-engages with [r]efresh after continuing (consistent with completed/failed).
16
+ */
17
+ export function isTerminalStatus(status) {
18
+ return status === "completed" || status === "failed" || status === "halted";
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onto-mcp",
3
- "version": "0.4.12",
3
+ "version": "0.4.13",
4
4
  "description": "MCP-native ontology review runtime with context-isolated lenses and controlled deliberation",
5
5
  "homepage": "https://github.com/kangminlee-maker/onto-mcp#readme",
6
6
  "bugs": {
@@ -49,10 +49,17 @@
49
49
  "check:invariant-change": "tsx scripts/check-invariant-change-marker.ts",
50
50
  "check:invariant-drift": "tsx scripts/check-invariant-drift.ts",
51
51
  "check:supported-models": "tsx scripts/check-supported-models.ts",
52
+ "check:prompt-projection-parity": "tsx scripts/check-prompt-projection-parity.ts",
53
+ "check:final-output-sections-parity": "tsx scripts/check-final-output-sections-parity.ts",
54
+ "check:obligation-coverage": "tsx scripts/check-obligation-coverage.ts",
55
+ "check:dispatch-fallback-package-parity": "tsx scripts/check-dispatch-fallback-package-parity.ts",
56
+ "check:graceful-signal-rethrow": "tsx scripts/check-graceful-signal-rethrow.ts",
52
57
  "test": "npm run test:e2e",
53
58
  "test:vitest": "vitest run",
54
59
  "test:review:live": "npm run build:ts-core && tsx scripts/review-mcp-live-e2e.ts",
55
60
  "test:review:live:cli": "npm run build:ts-core && tsx scripts/review-live-e2e.ts",
61
+ "test:reconstruct:dispatch-fallback:live": "node --env-file=.env --import tsx scripts/reconstruct-dispatch-fallback-live-e2e.mts",
62
+ "test:reconstruct:output-headroom:live": "node --env-file=.env --import tsx scripts/reconstruct-output-headroom-live-e2e.mts",
56
63
  "benchmark:review:pipeline": "tsx scripts/review-pipeline-benchmark.ts",
57
64
  "benchmark:reconstruct:pipeline": "tsx scripts/reconstruct-pipeline-benchmark.ts",
58
65
  "calibration:effort:report": "tsx scripts/effort-calibration-report.ts",
@@ -60,13 +67,21 @@
60
67
  "test:e2e": "npm run build:ts-core && tsx scripts/review-mcp-live-e2e.ts"
61
68
  },
62
69
  "dependencies": {
63
- "@anthropic-ai/sdk": "^0.99.0",
64
- "openai": "^6.33.0",
70
+ "@anthropic-ai/sdk": "0.99.0",
71
+ "fflate": "^0.8.3",
72
+ "ink": "^5.2.1",
73
+ "openai": "6.39.0",
74
+ "proper-lockfile": "^4.1.2",
75
+ "react": "^18.3.1",
76
+ "saxes": "^6.0.0",
65
77
  "yaml": "^2.8.2",
66
78
  "zod": "^4.3.6"
67
79
  },
68
80
  "devDependencies": {
69
81
  "@types/node": "^25.3.5",
82
+ "@types/proper-lockfile": "^4.1.4",
83
+ "@types/react": "^18.3.31",
84
+ "ink-testing-library": "^4.0.0",
70
85
  "tsx": "^4.21.0",
71
86
  "typescript": "^5.9.3",
72
87
  "vitest": "^4.0.18"