@playola-radio/slipstream 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +132 -0
- package/SKILL.md +63 -0
- package/THIRD-PARTY-NOTICES.md +36 -0
- package/contracts/interface/v2/schema.json +480 -0
- package/dist/answer-forwarder.js +58 -0
- package/dist/attribution-engine.js +164 -0
- package/dist/attribution-producer.js +153 -0
- package/dist/attribution-scoring.js +86 -0
- package/dist/attribution.js +120 -0
- package/dist/blob-source.js +100 -0
- package/dist/boundary-registry.js +47 -0
- package/dist/cas.js +99 -0
- package/dist/change-view.js +201 -0
- package/dist/claude-root-transcript.js +110 -0
- package/dist/cli.js +629 -0
- package/dist/clip-blob-reader.js +104 -0
- package/dist/clip-function-parser.js +83 -0
- package/dist/clip-language.js +15 -0
- package/dist/clip-projection-service.js +240 -0
- package/dist/clip-projection-worker.js +18 -0
- package/dist/clip-projection.js +585 -0
- package/dist/clip-worker-pool.js +118 -0
- package/dist/config.js +154 -0
- package/dist/control-client.js +98 -0
- package/dist/control-protocol.js +72 -0
- package/dist/daemon-location.js +33 -0
- package/dist/daemon.js +951 -0
- package/dist/display-fold.js +332 -0
- package/dist/engine.js +112 -0
- package/dist/entrypoint.js +17 -0
- package/dist/event.js +79 -0
- package/dist/evidence-ingest.js +60 -0
- package/dist/harness-context/claude.js +42 -0
- package/dist/harness-context/codex.js +49 -0
- package/dist/harness-context.js +49 -0
- package/dist/health.js +65 -0
- package/dist/http-reader.js +655 -0
- package/dist/http-security.js +50 -0
- package/dist/interface-projection.js +243 -0
- package/dist/interface-range-resolver.js +230 -0
- package/dist/interface-service.js +509 -0
- package/dist/interface-ts-pool.js +111 -0
- package/dist/interface-ts-worker.js +35 -0
- package/dist/interface-v2-comparison.js +135 -0
- package/dist/interface-v2-core.js +123 -0
- package/dist/interface-v2-typescript.js +448 -0
- package/dist/lock.js +197 -0
- package/dist/log-reader.js +148 -0
- package/dist/log.js +59 -0
- package/dist/maintenance.js +255 -0
- package/dist/mcp-forwarder.js +161 -0
- package/dist/mcp-protocol.js +92 -0
- package/dist/platform.js +21 -0
- package/dist/projection-admission.js +296 -0
- package/dist/projection-trace.js +17 -0
- package/dist/public-events.js +27 -0
- package/dist/question-hook.js +110 -0
- package/dist/questions.js +167 -0
- package/dist/reader-runtime.js +38 -0
- package/dist/reader.js +93 -0
- package/dist/recovery.js +303 -0
- package/dist/schema.js +108 -0
- package/dist/session.js +866 -0
- package/dist/sibling-module.js +10 -0
- package/dist/snapshot.js +15 -0
- package/dist/storage.js +111 -0
- package/dist/store-reader.js +185 -0
- package/dist/swift-grammar.js +268 -0
- package/dist/swift-interface-extract.js +344 -0
- package/dist/swift-interface.js +62 -0
- package/dist/swift-parse-host.js +174 -0
- package/dist/swift-parse-worker.js +34 -0
- package/dist/swift-parse.js +134 -0
- package/dist/swift-spans.js +62 -0
- package/dist/task-forwarder.js +152 -0
- package/dist/transcript/claude.js +199 -0
- package/dist/transcript/codex.js +132 -0
- package/dist/transcript/discovery.js +365 -0
- package/dist/transcript/file-reader.js +128 -0
- package/dist/transcript/fs-io.js +287 -0
- package/dist/transcript/runner.js +62 -0
- package/dist/transcript/steppers.js +30 -0
- package/dist/transcript/types.js +30 -0
- package/dist/transcript/watcher.js +133 -0
- package/dist/tui.js +168 -0
- package/package.json +60 -0
- package/schemas/projections/clip.v1.json +195 -0
- package/schemas/projections/clip.v2.json +212 -0
- package/schemas/projections/clip.v3.json +212 -0
- package/schemas/slipstream.capture.baseline.completed.v1.json +62 -0
- package/schemas/slipstream.capture.gap.v1.json +112 -0
- package/schemas/slipstream.change.attribution.v1.json +115 -0
- package/schemas/slipstream.enrichment.configured.v1.json +77 -0
- package/schemas/slipstream.enrichment.coverage.v1.json +95 -0
- package/schemas/slipstream.file.baselined.v1.json +122 -0
- package/schemas/slipstream.file.changed.v1.json +179 -0
- package/schemas/slipstream.harness.evidence.v1.json +145 -0
- package/schemas/slipstream.question.answered.v1.json +29 -0
- package/schemas/slipstream.question.dispatch_attempted.v1.json +28 -0
- package/schemas/slipstream.question.queued.v1.json +55 -0
- package/schemas/slipstream.session.resumed.v1.json +70 -0
- package/schemas/slipstream.session.started.v1.json +69 -0
- package/schemas/slipstream.task.started.v1.json +72 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://slipstream.dev/schemas/projections/interface.v2.json",
|
|
4
|
+
"title": "interface.v2",
|
|
5
|
+
"$comment": "Reader-derived, read-only comparison of the written function interfaces between two recorded cutoffs of one capture session (FUNCTION-CHANGES.md). No log event, seq or persisted index is created. Endpoints are recorded states, never the live filesystem or Git. Shared types, effects and behaviour are not analyzed. Cross-field invariants that src/schema.ts cannot express (if/else, maxItems, additionalProperties, nullable type arrays) are enforced by tools/interface-v2-contract.ts.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"projection_version",
|
|
9
|
+
"session_id",
|
|
10
|
+
"range",
|
|
11
|
+
"status",
|
|
12
|
+
"inventory",
|
|
13
|
+
"analysis",
|
|
14
|
+
"gaps",
|
|
15
|
+
"gaps_complete",
|
|
16
|
+
"files",
|
|
17
|
+
"page"
|
|
18
|
+
],
|
|
19
|
+
"properties": {
|
|
20
|
+
"projection_version": { "const": "interface.v2" },
|
|
21
|
+
"session_id": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"$comment": "Capture scope (which worktree is watched), never authorship. A comparison never spans sessions."
|
|
24
|
+
},
|
|
25
|
+
"range": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"required": ["before_seq", "after_seq"],
|
|
28
|
+
"properties": {
|
|
29
|
+
"before_seq": { "$ref": "#/$defs/cutoff" },
|
|
30
|
+
"after_seq": { "$ref": "#/$defs/cutoff" }
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"status": {
|
|
34
|
+
"enum": ["ready", "partial", "skipped"],
|
|
35
|
+
"$comment": "ready: every returned file is ready or identical (including an empty page), even if look-ahead was interrupted and page.complete is false; not a claim of complete inventory, history or semantics. partial: at least one returned file has another status. skipped: no file could start; files is empty and the cursor does not move."
|
|
36
|
+
},
|
|
37
|
+
"fallback_reason": {
|
|
38
|
+
"enum": ["overloaded", "timeout", "cancelled", "scan-limit"],
|
|
39
|
+
"$comment": "Present exactly when status is skipped."
|
|
40
|
+
},
|
|
41
|
+
"inventory": {
|
|
42
|
+
"oneOf": [{ "$ref": "#/$defs/resolvedInventory" }, { "const": null }],
|
|
43
|
+
"$comment": "null only when a skipped page stopped before the recorded range was resolved. No inventory claim was evaluated."
|
|
44
|
+
},
|
|
45
|
+
"analysis": { "$ref": "#/$defs/analysis" },
|
|
46
|
+
"gaps": {
|
|
47
|
+
"oneOf": [{ "type": "array", "items": { "$ref": "#/$defs/gap" }, "$comment": "Every capture.gap at or before after_seq, ordered by seq, subject to the shared 64 KiB metadata budget." }, { "const": null }],
|
|
48
|
+
"$comment": "null only when a skipped page stopped before the recorded range was resolved."
|
|
49
|
+
},
|
|
50
|
+
"gaps_complete": { "type": "boolean" },
|
|
51
|
+
"files": {
|
|
52
|
+
"type": "array",
|
|
53
|
+
"items": { "$ref": "#/$defs/fileResult" },
|
|
54
|
+
"$comment": "Ordered by path, by exact UTF-16 code unit. identical results appear only with include_identical=true."
|
|
55
|
+
},
|
|
56
|
+
"page": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"required": ["complete", "next_after_path"],
|
|
59
|
+
"properties": {
|
|
60
|
+
"complete": {
|
|
61
|
+
"type": "boolean",
|
|
62
|
+
"$comment": "true only when the scan proves this is the last page. false after look-ahead interruption, even if a continuation later finds no visible rows. Loaded totals are partial until complete is true."
|
|
63
|
+
},
|
|
64
|
+
"next_after_path": {
|
|
65
|
+
"oneOf": [{ "type": "string" }, { "const": null }],
|
|
66
|
+
"$comment": "Exclusive cursor for the next request. null when complete. After a look-ahead interruption it is the last returned path, never an unchecked candidate. On a skipped page it repeats the request's after_path (null when none), so the cursor does not move."
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"$defs": {
|
|
72
|
+
"resolvedInventory": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"required": [
|
|
75
|
+
"scope",
|
|
76
|
+
"baseline_completed_seq",
|
|
77
|
+
"unknown_scopes",
|
|
78
|
+
"unknown_scopes_complete",
|
|
79
|
+
"policy_exclusions"
|
|
80
|
+
],
|
|
81
|
+
"properties": {
|
|
82
|
+
"scope": {
|
|
83
|
+
"const": "observed",
|
|
84
|
+
"$comment": "Only paths recorded in this session are compared (F2-A). Never a complete repository inventory."
|
|
85
|
+
},
|
|
86
|
+
"baseline_completed_seq": {
|
|
87
|
+
"oneOf": [{ "$ref": "#/$defs/seq" }, { "const": null }],
|
|
88
|
+
"$comment": "seq of capture.baseline.completed when it is at or before after_seq, else null. A value greater than before_seq means the baseline was still running at the before cutoff."
|
|
89
|
+
},
|
|
90
|
+
"unknown_scopes": {
|
|
91
|
+
"type": "array",
|
|
92
|
+
"items": { "type": "string" },
|
|
93
|
+
"$comment": "From capture.baseline.completed, ordered by UTF-16 code unit; empty when the baseline is not complete by after_seq."
|
|
94
|
+
},
|
|
95
|
+
"unknown_scopes_complete": {
|
|
96
|
+
"type": "boolean",
|
|
97
|
+
"$comment": "false when entries were omitted by the shared 64 KiB metadata budget; the full list is on the event stream."
|
|
98
|
+
},
|
|
99
|
+
"policy_exclusions": {
|
|
100
|
+
"type": "array",
|
|
101
|
+
"items": { "enum": ["store-directory", ".git", "symlinks"] },
|
|
102
|
+
"$comment": "Static capture policy, always exactly [\"store-directory\", \".git\", \"symlinks\"] (checked in code; src/schema.ts const is not deep): these targets are never read, and the baseline records no event for them."
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"analysis": {
|
|
107
|
+
"type": "object",
|
|
108
|
+
"required": ["shared_types", "effects", "behavior"],
|
|
109
|
+
"properties": {
|
|
110
|
+
"shared_types": { "const": "notAnalyzed" },
|
|
111
|
+
"effects": { "const": "notAnalyzed" },
|
|
112
|
+
"behavior": { "const": "notAnalyzed" }
|
|
113
|
+
},
|
|
114
|
+
"$comment": "Decisions F3-A and F4-A. Analyzing any of these is a new projection version."
|
|
115
|
+
},
|
|
116
|
+
"seq": { "type": "string", "pattern": "^[1-9][0-9]*$" },
|
|
117
|
+
"cutoff": {
|
|
118
|
+
"type": "string",
|
|
119
|
+
"pattern": "^(0|[1-9][0-9]*)$",
|
|
120
|
+
"$comment": "Decimal string, arbitrary precision. 0 means the empty recorded prefix, not an empty repository."
|
|
121
|
+
},
|
|
122
|
+
"snapshot": {
|
|
123
|
+
"$comment": "The recorded snapshot tag, identical to the file.changed / file.baselined event shape.",
|
|
124
|
+
"oneOf": [
|
|
125
|
+
{
|
|
126
|
+
"type": "object",
|
|
127
|
+
"required": ["kind", "sha256", "size"],
|
|
128
|
+
"properties": {
|
|
129
|
+
"kind": { "const": "content" },
|
|
130
|
+
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
|
131
|
+
"size": { "type": "integer", "minimum": 0 }
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"type": "object",
|
|
136
|
+
"required": ["kind"],
|
|
137
|
+
"properties": { "kind": { "const": "absent" } }
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"type": "object",
|
|
141
|
+
"required": ["kind", "reason"],
|
|
142
|
+
"properties": {
|
|
143
|
+
"kind": { "const": "unavailable" },
|
|
144
|
+
"reason": { "enum": ["oversize", "unreadable", "unstable", "io-error", "baseline-unknown"] }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
"gap": {
|
|
150
|
+
"type": "object",
|
|
151
|
+
"required": ["seq", "reason", "scope"],
|
|
152
|
+
"properties": {
|
|
153
|
+
"seq": { "$ref": "#/$defs/seq" },
|
|
154
|
+
"reason": { "enum": ["coalesced", "baseline-unreadable", "watcher-error", "restart", "storage"] },
|
|
155
|
+
"scope": {
|
|
156
|
+
"oneOf": [
|
|
157
|
+
{
|
|
158
|
+
"type": "object",
|
|
159
|
+
"required": ["kind"],
|
|
160
|
+
"properties": { "kind": { "const": "session" } }
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"type": "object",
|
|
164
|
+
"required": ["kind", "path"],
|
|
165
|
+
"properties": {
|
|
166
|
+
"kind": { "enum": ["directory", "path"] },
|
|
167
|
+
"path": { "type": "string" }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"endpoint": {
|
|
175
|
+
"oneOf": [
|
|
176
|
+
{
|
|
177
|
+
"type": "object",
|
|
178
|
+
"required": ["kind", "record_seq", "field", "snapshot"],
|
|
179
|
+
"properties": {
|
|
180
|
+
"kind": { "const": "recorded" },
|
|
181
|
+
"record_seq": {
|
|
182
|
+
"$ref": "#/$defs/seq",
|
|
183
|
+
"$comment": "The event that recorded this state. For the first-change predecessor rule it is the later file.changed, with field before, so the response never claims the state was recorded by before_seq."
|
|
184
|
+
},
|
|
185
|
+
"field": {
|
|
186
|
+
"enum": ["snapshot", "before", "after"],
|
|
187
|
+
"$comment": "snapshot: file.baselined. before/after: that side of a file.changed."
|
|
188
|
+
},
|
|
189
|
+
"snapshot": { "$ref": "#/$defs/snapshot" },
|
|
190
|
+
"observation": {
|
|
191
|
+
"enum": ["watcher", "reconciliation"],
|
|
192
|
+
"$comment": "Present exactly when the record is a file.changed."
|
|
193
|
+
},
|
|
194
|
+
"gap_ref": {
|
|
195
|
+
"$ref": "#/$defs/seq",
|
|
196
|
+
"$comment": "Copied from a reconciliation record. Never a claim that the outage's intermediate edits were recovered."
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"type": "object",
|
|
202
|
+
"required": ["kind"],
|
|
203
|
+
"properties": {
|
|
204
|
+
"kind": {
|
|
205
|
+
"const": "unknownBoundary",
|
|
206
|
+
"$comment": "Projection-derived: the path has no record at or before before_seq and the first-change predecessor rule does not apply. There is no provenance because no such record exists."
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
},
|
|
212
|
+
"coverageSide": {
|
|
213
|
+
"type": "object",
|
|
214
|
+
"required": ["state"],
|
|
215
|
+
"properties": {
|
|
216
|
+
"state": {
|
|
217
|
+
"enum": ["complete", "absent", "incomplete", "unavailable", "unsupported", "notEvaluated"],
|
|
218
|
+
"$comment": "Each side reports its own extraction state even when its reason lost status precedence. identical content is notEvaluated (no parse happened)."
|
|
219
|
+
},
|
|
220
|
+
"reason": {
|
|
221
|
+
"type": "string",
|
|
222
|
+
"$comment": "Present exactly for incomplete and unavailable, e.g. parse-error, blob-missing, unknown-boundary, oversize."
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
"fileResult": {
|
|
227
|
+
"type": "object",
|
|
228
|
+
"required": ["path", "before", "after", "language", "language_version", "status", "coverage", "changes"],
|
|
229
|
+
"properties": {
|
|
230
|
+
"path": { "type": "string" },
|
|
231
|
+
"before": { "$ref": "#/$defs/endpoint" },
|
|
232
|
+
"after": {
|
|
233
|
+
"$ref": "#/$defs/endpoint",
|
|
234
|
+
"$comment": "Always recorded: a listed path has a record at or before after_seq."
|
|
235
|
+
},
|
|
236
|
+
"language": {
|
|
237
|
+
"oneOf": [{ "type": "string" }, { "const": null }],
|
|
238
|
+
"$comment": "Plain string, never an enum. null exactly when no language module exists for the path, whatever the status."
|
|
239
|
+
},
|
|
240
|
+
"language_version": {
|
|
241
|
+
"oneOf": [{ "type": "string" }, { "const": null }],
|
|
242
|
+
"$comment": "e.g. typescript.v2, swift.v1. null exactly when language is null."
|
|
243
|
+
},
|
|
244
|
+
"status": {
|
|
245
|
+
"enum": ["identical", "ready", "incomplete", "unavailable", "unsupported", "skipped"],
|
|
246
|
+
"$comment": "First established condition wins (FUNCTION-CHANGES.md §4.4): identical, before/after incomplete, duplicate-declaration, ambiguous-correspondence, before/after unavailable, unsupported, too-large, timeout/cancelled, ready."
|
|
247
|
+
},
|
|
248
|
+
"fallback_reason": {
|
|
249
|
+
"type": "string",
|
|
250
|
+
"$comment": "Present exactly for incomplete, unavailable, unsupported and skipped. Absent for identical and ready."
|
|
251
|
+
},
|
|
252
|
+
"coverage": {
|
|
253
|
+
"type": "object",
|
|
254
|
+
"required": ["before", "after"],
|
|
255
|
+
"properties": {
|
|
256
|
+
"before": { "$ref": "#/$defs/coverageSide" },
|
|
257
|
+
"after": { "$ref": "#/$defs/coverageSide" }
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
"changes": {
|
|
261
|
+
"type": "array",
|
|
262
|
+
"items": { "$ref": "#/$defs/change" },
|
|
263
|
+
"$comment": "Non-empty only when status is ready (DA-3, per file). Ordered removed, signatureChanged, added, then by the D8 tuple."
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
"change": {
|
|
268
|
+
"type": "object",
|
|
269
|
+
"required": ["kind", "identity", "before", "after", "parameters", "result", "throws", "header"],
|
|
270
|
+
"properties": {
|
|
271
|
+
"kind": { "enum": ["added", "removed", "signatureChanged"] },
|
|
272
|
+
"identity": { "$ref": "#/$defs/identity" },
|
|
273
|
+
"before": {
|
|
274
|
+
"oneOf": [{ "$ref": "#/$defs/declarationSide" }, { "const": null }],
|
|
275
|
+
"$comment": "null for added."
|
|
276
|
+
},
|
|
277
|
+
"after": {
|
|
278
|
+
"oneOf": [{ "$ref": "#/$defs/declarationSide" }, { "const": null }],
|
|
279
|
+
"$comment": "null for removed."
|
|
280
|
+
},
|
|
281
|
+
"parameters": {
|
|
282
|
+
"type": "array",
|
|
283
|
+
"items": { "$ref": "#/$defs/parameterDelta" },
|
|
284
|
+
"$comment": "Rows with a before side in before-position order, then added rows in after-position order. A parameter pairs only by a written local name occurring exactly once on each side."
|
|
285
|
+
},
|
|
286
|
+
"result": {
|
|
287
|
+
"oneOf": [{ "$ref": "#/$defs/resultDelta" }, { "const": null }],
|
|
288
|
+
"$comment": "null when neither side has a result slot (a TypeScript constructor)."
|
|
289
|
+
},
|
|
290
|
+
"throws": { "$ref": "#/$defs/throwsDelta" },
|
|
291
|
+
"header": {
|
|
292
|
+
"$ref": "#/$defs/headerDelta",
|
|
293
|
+
"$comment": "Every part of the written header not covered by parameters, result or throws."
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
"identity": {
|
|
298
|
+
"type": "object",
|
|
299
|
+
"required": ["kind", "scope", "name", "guards"],
|
|
300
|
+
"properties": {
|
|
301
|
+
"kind": { "type": "string" },
|
|
302
|
+
"scope": {
|
|
303
|
+
"type": "array",
|
|
304
|
+
"items": {
|
|
305
|
+
"type": "object",
|
|
306
|
+
"required": ["kind", "name"],
|
|
307
|
+
"properties": { "kind": { "type": "string" }, "name": { "type": "string" } }
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
"name": { "type": "string" },
|
|
311
|
+
"guards": { "type": "array", "items": { "type": "string" } }
|
|
312
|
+
},
|
|
313
|
+
"$comment": "A matching key scoped to the path, not proof of rename continuity. No overload ordinal or line number."
|
|
314
|
+
},
|
|
315
|
+
"declarationSide": {
|
|
316
|
+
"type": "object",
|
|
317
|
+
"required": ["display_name", "span"],
|
|
318
|
+
"properties": {
|
|
319
|
+
"display_name": { "type": "string" },
|
|
320
|
+
"span": { "$ref": "#/$defs/span" }
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
"span": {
|
|
324
|
+
"type": "object",
|
|
325
|
+
"required": ["byte_start", "byte_end"],
|
|
326
|
+
"properties": {
|
|
327
|
+
"byte_start": { "type": "integer", "minimum": 0 },
|
|
328
|
+
"byte_end": { "type": "integer", "minimum": 0 }
|
|
329
|
+
},
|
|
330
|
+
"$comment": "[byte_start, byte_end) over the raw UTF-8 bytes of that side's endpoint blob."
|
|
331
|
+
},
|
|
332
|
+
"op": {
|
|
333
|
+
"enum": ["equal", "added", "removed", "changed"],
|
|
334
|
+
"$comment": "Sides carry normalized token text. equal: both sides present and identical. added: before null. removed: after null. changed: both present and different."
|
|
335
|
+
},
|
|
336
|
+
"typeRef": {
|
|
337
|
+
"oneOf": [
|
|
338
|
+
{
|
|
339
|
+
"type": "object",
|
|
340
|
+
"required": ["state", "text"],
|
|
341
|
+
"properties": {
|
|
342
|
+
"state": { "enum": ["written", "implicit"] },
|
|
343
|
+
"text": { "type": "string" }
|
|
344
|
+
},
|
|
345
|
+
"$comment": "written: the annotation in the source (a TypeScript annotation spelled unknown is written). implicit: defined by the language, e.g. Swift Void for an omitted return clause."
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
"type": "object",
|
|
349
|
+
"required": ["state", "reason"],
|
|
350
|
+
"properties": {
|
|
351
|
+
"state": { "const": "unknown" },
|
|
352
|
+
"reason": { "const": "inferred-not-computed" }
|
|
353
|
+
},
|
|
354
|
+
"$comment": "Nothing is written and the type would be inferred. Extraction is complete; the type is unknown."
|
|
355
|
+
}
|
|
356
|
+
]
|
|
357
|
+
},
|
|
358
|
+
"parameter": {
|
|
359
|
+
"type": "object",
|
|
360
|
+
"required": ["position", "label", "name", "binding", "type", "optional", "variadic", "default", "modifiers"],
|
|
361
|
+
"properties": {
|
|
362
|
+
"position": { "type": "integer", "minimum": 0 },
|
|
363
|
+
"label": {
|
|
364
|
+
"oneOf": [{ "type": "string" }, { "const": null }],
|
|
365
|
+
"$comment": "A separately written Swift argument label (including _). null when none is written, and always null in TypeScript."
|
|
366
|
+
},
|
|
367
|
+
"name": {
|
|
368
|
+
"type": "string",
|
|
369
|
+
"$comment": "The written local name, or the whole written destructuring pattern."
|
|
370
|
+
},
|
|
371
|
+
"binding": {
|
|
372
|
+
"enum": ["identifier", "pattern", "wildcard"],
|
|
373
|
+
"$comment": "pattern and wildcard (Swift _) have no pairing key and are never paired."
|
|
374
|
+
},
|
|
375
|
+
"type": { "$ref": "#/$defs/typeRef" },
|
|
376
|
+
"optional": { "type": "boolean", "$comment": "TypeScript ?." },
|
|
377
|
+
"variadic": { "type": "boolean", "$comment": "TypeScript rest or Swift variadic." },
|
|
378
|
+
"default": {
|
|
379
|
+
"oneOf": [{ "type": "string" }, { "const": null }],
|
|
380
|
+
"$comment": "Written default expression syntax, or null."
|
|
381
|
+
},
|
|
382
|
+
"modifiers": {
|
|
383
|
+
"type": "array",
|
|
384
|
+
"items": { "type": "string" },
|
|
385
|
+
"$comment": "As written, e.g. Swift inout, TypeScript readonly/public parameter properties."
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
"parameterDelta": {
|
|
390
|
+
"type": "object",
|
|
391
|
+
"required": ["op", "before", "after"],
|
|
392
|
+
"properties": {
|
|
393
|
+
"op": { "$ref": "#/$defs/op" },
|
|
394
|
+
"before": { "oneOf": [{ "$ref": "#/$defs/parameter" }, { "const": null }] },
|
|
395
|
+
"after": { "oneOf": [{ "$ref": "#/$defs/parameter" }, { "const": null }] }
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
"result": {
|
|
399
|
+
"oneOf": [
|
|
400
|
+
{
|
|
401
|
+
"type": "object",
|
|
402
|
+
"required": ["kind", "type"],
|
|
403
|
+
"properties": {
|
|
404
|
+
"kind": { "const": "return" },
|
|
405
|
+
"type": { "$ref": "#/$defs/typeRef" }
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
"type": "object",
|
|
410
|
+
"required": ["kind", "failable"],
|
|
411
|
+
"properties": {
|
|
412
|
+
"kind": { "const": "initializer" },
|
|
413
|
+
"failable": {
|
|
414
|
+
"oneOf": [{ "enum": ["?", "!"] }, { "const": null }],
|
|
415
|
+
"$comment": "Swift init? / init!; null for a non-failable init. -> Self is never fabricated."
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
]
|
|
420
|
+
},
|
|
421
|
+
"resultDelta": {
|
|
422
|
+
"type": "object",
|
|
423
|
+
"required": ["op", "before", "after"],
|
|
424
|
+
"properties": {
|
|
425
|
+
"op": { "$ref": "#/$defs/op" },
|
|
426
|
+
"before": { "oneOf": [{ "$ref": "#/$defs/result" }, { "const": null }] },
|
|
427
|
+
"after": { "oneOf": [{ "$ref": "#/$defs/result" }, { "const": null }] }
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
"throwsClause": {
|
|
431
|
+
"type": "object",
|
|
432
|
+
"required": ["mode"],
|
|
433
|
+
"properties": {
|
|
434
|
+
"mode": {
|
|
435
|
+
"enum": ["notExpressible", "none", "throws", "rethrows"],
|
|
436
|
+
"$comment": "notExpressible: TypeScript has no throws clause; never read as does-not-throw. none: Swift with no throws clause. throws/rethrows: as written; a declared throws does not mean the function actually throws."
|
|
437
|
+
},
|
|
438
|
+
"type": {
|
|
439
|
+
"type": "string",
|
|
440
|
+
"$comment": "Swift typed throws(E) only, when the pinned grammar parses it clean."
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
"throwsDelta": {
|
|
445
|
+
"type": "object",
|
|
446
|
+
"required": ["op", "before", "after"],
|
|
447
|
+
"properties": {
|
|
448
|
+
"op": { "$ref": "#/$defs/op" },
|
|
449
|
+
"before": { "oneOf": [{ "$ref": "#/$defs/throwsClause" }, { "const": null }] },
|
|
450
|
+
"after": { "oneOf": [{ "$ref": "#/$defs/throwsClause" }, { "const": null }] }
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
"header": {
|
|
454
|
+
"type": "object",
|
|
455
|
+
"required": ["modifiers", "generic_parameters", "constraints"],
|
|
456
|
+
"properties": {
|
|
457
|
+
"modifiers": {
|
|
458
|
+
"type": "array",
|
|
459
|
+
"items": { "type": "string" },
|
|
460
|
+
"$comment": "Written order, e.g. export, async, static, public, mutating."
|
|
461
|
+
},
|
|
462
|
+
"generic_parameters": { "type": "array", "items": { "type": "string" } },
|
|
463
|
+
"constraints": {
|
|
464
|
+
"type": "array",
|
|
465
|
+
"items": { "type": "string" },
|
|
466
|
+
"$comment": "Swift where-clause requirements, as written."
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
"headerDelta": {
|
|
471
|
+
"type": "object",
|
|
472
|
+
"required": ["op", "before", "after"],
|
|
473
|
+
"properties": {
|
|
474
|
+
"op": { "$ref": "#/$defs/op" },
|
|
475
|
+
"before": { "oneOf": [{ "$ref": "#/$defs/header" }, { "const": null }] },
|
|
476
|
+
"after": { "oneOf": [{ "$ref": "#/$defs/header" }, { "const": null }] }
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* answer_question orchestration for the MCP forwarder.
|
|
3
|
+
*
|
|
4
|
+
* The daemon keeps one immutable answer per question and replays an identical
|
|
5
|
+
* text as a duplicate, so a lost or malformed reply after the request was sent is
|
|
6
|
+
* resolved by exactly one identical resend. On that resend only a structural
|
|
7
|
+
* rejection proves nothing was recorded; anything else (a replaced capture, a
|
|
8
|
+
* storage fault, a second lost reply) may follow a first send that committed, so
|
|
9
|
+
* it collapses to OUTCOME_UNKNOWN instead of implying the answer was not recorded.
|
|
10
|
+
*/
|
|
11
|
+
import { OutcomeUnknownError } from "./control-client.js";
|
|
12
|
+
import { isResolved } from "./harness-context.js";
|
|
13
|
+
const DEFINITIVE_ON_RESEND = new Set(['INVALID_ANSWER', 'IDENTITY_UNRESOLVED', 'ANSWER_CONFLICT']);
|
|
14
|
+
/** Only a complete ack naming a durable event proves the answer was recorded; any
|
|
15
|
+
* other `ok:true` shape is post-send ambiguity, resolved like a lost reply. */
|
|
16
|
+
function recordedAck(res, questionId) {
|
|
17
|
+
if (!res.ok || res.question_id !== questionId || typeof res.seq !== 'string' || !/^[1-9][0-9]*$/.test(res.seq)
|
|
18
|
+
|| res.event_id !== res.seq || typeof res.session_id !== 'string' || res.session_id.length === 0
|
|
19
|
+
|| !Number.isSafeInteger(res.answered_at_ms) || typeof res.duplicate !== 'boolean')
|
|
20
|
+
return null;
|
|
21
|
+
const { v: _v, ok: _ok, ...ack } = res;
|
|
22
|
+
return ack;
|
|
23
|
+
}
|
|
24
|
+
function failure(code, message) {
|
|
25
|
+
return { text: `${code}: ${message}`, isError: true, structured: { code } };
|
|
26
|
+
}
|
|
27
|
+
const outcomeUnknown = () => failure('OUTCOME_UNKNOWN', 'the answer was sent but whether it was recorded could not be confirmed; resending the identical text is safe, different text is not');
|
|
28
|
+
export async function forwardAnswer(deps) {
|
|
29
|
+
if (!isResolved(deps.identity))
|
|
30
|
+
return failure('IDENTITY_UNRESOLVED', deps.identity.unresolved);
|
|
31
|
+
const envelope = {
|
|
32
|
+
v: 1,
|
|
33
|
+
verb: 'answer_question',
|
|
34
|
+
question_id: deps.questionId,
|
|
35
|
+
text: deps.text,
|
|
36
|
+
harness: deps.identity.harness,
|
|
37
|
+
harness_session_id: deps.identity.harness_session_id,
|
|
38
|
+
worktree: deps.identity.worktree,
|
|
39
|
+
};
|
|
40
|
+
for (const resend of [false, true]) {
|
|
41
|
+
let res;
|
|
42
|
+
try {
|
|
43
|
+
res = await deps.send(envelope);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
if (err instanceof OutcomeUnknownError)
|
|
47
|
+
continue;
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
const ack = recordedAck(res, deps.questionId);
|
|
51
|
+
if (ack) {
|
|
52
|
+
return { text: `Answer recorded for question ${deps.questionId} (seq ${String(ack.seq)}).`, isError: false, structured: ack };
|
|
53
|
+
}
|
|
54
|
+
if (!res.ok && (!resend || DEFINITIVE_ON_RESEND.has(res.code)))
|
|
55
|
+
return failure(res.code, res.message);
|
|
56
|
+
}
|
|
57
|
+
return outcomeUnknown();
|
|
58
|
+
}
|