@orkestrel/probe 0.0.1

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.
@@ -0,0 +1,735 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _orkestrel_contract = require("@orkestrel/contract");
3
+ //#region src/core/constants.ts
4
+ /**
5
+ * Lists the stages a claim passes through, in the order a verdict reports them.
6
+ *
7
+ * @remarks
8
+ * One list feeds the `Stage` type, the wire shape, the stage guard, and the receipt computation, so
9
+ * a stage cannot be admitted by one and refused by another. The type derives from this list rather
10
+ * than standing beside it, so a stage the list omits is a stage no member of the union names.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * PROBE_STAGES // ['type', 'lint', 'runtime']
15
+ * ```
16
+ */
17
+ var PROBE_STAGES = Object.freeze([
18
+ "type",
19
+ "lint",
20
+ "runtime"
21
+ ]);
22
+ /**
23
+ * Lists the parties that can own action on an issue or probe failure.
24
+ *
25
+ * @remarks
26
+ * One list feeds the `Party` type and its guard, so a party cannot be admitted by one and refused
27
+ * by another.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * PROBE_PARTIES // ['claimant', 'workspace', 'instrument']
32
+ * ```
33
+ */
34
+ var PROBE_PARTIES = Object.freeze([
35
+ "claimant",
36
+ "workspace",
37
+ "instrument"
38
+ ]);
39
+ /**
40
+ * Lists the conditions that can end a probe operation.
41
+ *
42
+ * @remarks
43
+ * One list feeds the code union and the error guard, so a condition cannot be constructed by one
44
+ * and refused by the other. The guard reads this list rather than the class, which is what lets it
45
+ * refuse a lookalike carrying a condition this package never declared.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * PROBE_ERROR_CODES // ['refused', 'missing', 'malformed', 'destroyed', 'deadline']
50
+ * ```
51
+ */
52
+ var PROBE_ERROR_CODES = Object.freeze([
53
+ "refused",
54
+ "missing",
55
+ "malformed",
56
+ "destroyed",
57
+ "deadline"
58
+ ]);
59
+ /**
60
+ * Names the leading token every receipt carries.
61
+ *
62
+ * @remarks
63
+ * A receipt travels away from the verdict that minted it — an agent pastes it into the promotion
64
+ * action — so it names itself rather than relying on where it was found.
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * const receipt =
69
+ * 'probe:6ca20c3bff623031d3955b9d1a76d71d:type:typescript@6.0.3:oxlint@1.79.0:vitest@4.1.11:configs/src/tsconfig.core.json@3b674fdf121c85efb9ed1bab25ceeec8'
70
+ * receipt.startsWith(RECEIPT_PREFIX) // true
71
+ * ```
72
+ */
73
+ var RECEIPT_PREFIX = "probe";
74
+ /**
75
+ * Names the character joining a receipt's tokens.
76
+ *
77
+ * @remarks
78
+ * A project path can contain this character, so the project field goes last and a reader rejoins
79
+ * every field from index 6 onward with this separator rather than expecting a fixed count.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * const receipt =
84
+ * 'probe:6ca20c3bff623031d3955b9d1a76d71d:type:typescript@6.0.3:oxlint@1.79.0:vitest@4.1.11:configs/src/tsconfig.core.json@3b674fdf121c85efb9ed1bab25ceeec8'
85
+ * receipt.split(RECEIPT_SEPARATOR).length // 7
86
+ * ```
87
+ */
88
+ var RECEIPT_SEPARATOR = ":";
89
+ //#endregion
90
+ //#region src/core/errors.ts
91
+ /**
92
+ * Reports one probe failure under stable ownership and condition axes.
93
+ *
94
+ * @remarks
95
+ * Every failure raised while serving a claim arrives as one of these, so a consumer catching from
96
+ * `prove`, from a stage, or from a workspace leaf branches on {@link Party} and
97
+ * {@link ProbeErrorCode} rather than on message text. Narrow a caught value with
98
+ * {@link isProbeError} before reading either member. A pure leaf called directly with a value
99
+ * `isClaim` would refuse raises the host's own fault unchanged, because those leaves are written to
100
+ * be total over a validated claim rather than over every value.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * const error = new ProbeError('Path escapes the workspace: ../secrets.env', {
105
+ * origin: 'claimant',
106
+ * code: 'refused',
107
+ * context: { path: '../secrets.env' },
108
+ * })
109
+ * error.origin // 'claimant'
110
+ * error.code // 'refused'
111
+ * error.context?.path // '../secrets.env'
112
+ * ```
113
+ */
114
+ var ProbeError = class extends Error {
115
+ name = "ProbeError";
116
+ origin;
117
+ code;
118
+ context;
119
+ /**
120
+ * Creates one classified probe failure.
121
+ *
122
+ * @param message - Human-readable description of what failed
123
+ * @param options - The ownership, condition, optional structured context, and optional cause
124
+ */
125
+ constructor(message, options) {
126
+ const cause = options.cause;
127
+ super(message, cause === void 0 ? void 0 : { cause });
128
+ Object.defineProperty(this, Symbol.for("@orkestrel/probe.error"), { value: true });
129
+ this.origin = options.origin;
130
+ this.code = options.code;
131
+ if (options.context !== void 0) this.context = options.context;
132
+ }
133
+ };
134
+ /**
135
+ * Checks whether an unknown value is a {@link ProbeError}.
136
+ *
137
+ * @remarks
138
+ * Recognition combines the global own-property brand with the native `Error` base, the subclass
139
+ * prototype, the fixed name, a party {@link PROBE_PARTIES} declares, and a condition
140
+ * {@link PROBE_ERROR_CODES} declares. The brand carries recognition across duplicate installations
141
+ * and across an ESM and a CommonJS copy of this package, where the two classes are different values.
142
+ * A plain `Error`, a property-only lookalike, and a branded value carrying an undeclared origin or
143
+ * condition all stay outside the type.
144
+ *
145
+ * @param value - The value to inspect
146
+ * @returns True only for a `ProbeError` instance; false otherwise
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * isProbeError(createDestroyedError('probe')) // true
151
+ * isProbeError(new Error('The probe has been destroyed')) // false
152
+ * ```
153
+ */
154
+ function isProbeError(value) {
155
+ if (!(0, _orkestrel_contract.isError)(value)) return false;
156
+ return (0, _orkestrel_contract.holds)(() => {
157
+ if (Object.getPrototypeOf(value) === Error.prototype) return false;
158
+ if (value.name !== "ProbeError" || !("origin" in value) || !("code" in value)) return false;
159
+ if (Object.getOwnPropertyDescriptor(value, Symbol.for("@orkestrel/probe.error"))?.value !== true) return false;
160
+ const origin = value.origin;
161
+ const code = value.code;
162
+ return PROBE_PARTIES.some((declared) => declared === origin) && PROBE_ERROR_CODES.some((declared) => declared === code);
163
+ });
164
+ }
165
+ /**
166
+ * Creates the failure raised when an instrument is used after it was torn down.
167
+ *
168
+ * @remarks
169
+ * Every resident entity in this package refuses this way, so the subject names which one in the
170
+ * message and the condition is the same for all of them. A caller that meets this builds a
171
+ * replacement rather than retrying: teardown is permanent.
172
+ *
173
+ * @param subject - The torn-down entity, named as the message names it, such as `type stage`
174
+ * @returns A typed teardown failure
175
+ *
176
+ * @example
177
+ * ```ts
178
+ * createDestroyedError('type stage').message // 'The type stage has been destroyed'
179
+ * createDestroyedError('probe').code // 'destroyed'
180
+ * ```
181
+ */
182
+ function createDestroyedError(subject) {
183
+ return new ProbeError(`The ${subject} has been destroyed`, {
184
+ origin: "claimant",
185
+ code: "destroyed"
186
+ });
187
+ }
188
+ //#endregion
189
+ //#region src/core/validators.ts
190
+ /**
191
+ * Checks whether a value names a stage.
192
+ *
193
+ * @param value - The value to check
194
+ * @returns True if the value is a stage name; false otherwise
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * isStage('lint') // true
199
+ * isStage('typecheck') // false
200
+ * ```
201
+ */
202
+ var isStage = (0, _orkestrel_contract.literalOf)(PROBE_STAGES);
203
+ /**
204
+ * Checks whether a value carries a proposed file's path and text.
205
+ *
206
+ * @remarks
207
+ * Rejects an empty, absolute, or workspace-escaping path. A contained relative path may contain
208
+ * `.` or may traverse to a parent that remains inside the workspace. Every stage resolves a file
209
+ * by that path, so admitting an escape here would defer the refusal until a stage had already
210
+ * accepted the draft. An `Issue` carries no such minimum because this package produces issues
211
+ * rather than admits them.
212
+ *
213
+ * @param value - The value to check
214
+ * @returns True if the value is a draft; false otherwise
215
+ *
216
+ * @example
217
+ * ```ts
218
+ * isDraft({ path: 'src/core/greeting.ts', text: 'export const GREETING = "hi"\n' }) // true
219
+ * isDraft({ path: '../../etc/hosts', text: '' }) // false
220
+ * isDraft({ path: 'src/core/greeting.ts' }) // false
221
+ * ```
222
+ */
223
+ var isDraft = (0, _orkestrel_contract.recordOf)({
224
+ path: (0, _orkestrel_contract.andOf)(_orkestrel_contract.isNonEmptyString, (path) => {
225
+ if (/^(?:[\\/]|[A-Za-z]:)/.test(path)) return false;
226
+ let depth = 0;
227
+ for (const segment of path.split(/[\\/]+/)) {
228
+ if (segment === "" || segment === ".") continue;
229
+ if (segment !== "..") {
230
+ depth += 1;
231
+ continue;
232
+ }
233
+ if (depth === 0) return false;
234
+ depth -= 1;
235
+ }
236
+ return true;
237
+ }),
238
+ text: _orkestrel_contract.isString
239
+ });
240
+ /**
241
+ * Checks whether a value carries a claim's candidate drafts and its test.
242
+ *
243
+ * @param value - The value to check
244
+ * @returns True if the value is a case; false otherwise
245
+ *
246
+ * @example
247
+ * ```ts
248
+ * const test = { path: 'tests/src/core/greeting.test.ts', text: 'test("greets", () => {})\n' }
249
+ * isCase({ files: [], test }) // true
250
+ * ```
251
+ */
252
+ var isCase = (0, _orkestrel_contract.recordOf)({
253
+ files: (0, _orkestrel_contract.arrayOf)(isDraft),
254
+ test: isDraft
255
+ });
256
+ /**
257
+ * Checks whether a value carries a case plus the stage and reason it must fail for.
258
+ *
259
+ * @param value - The value to check
260
+ * @returns True if the value is a control; false otherwise
261
+ *
262
+ * @example
263
+ * ```ts
264
+ * const test = { path: 'tests/src/core/greeting.test.ts', text: 'test("greets", () => {})\n' }
265
+ * isControl({ files: [], test, stage: 'type', reason: 'must not compile' }) // true
266
+ * isControl({ files: [], test }) // false
267
+ * ```
268
+ */
269
+ var isControl = (0, _orkestrel_contract.recordOf)({
270
+ files: (0, _orkestrel_contract.arrayOf)(isDraft),
271
+ test: isDraft,
272
+ stage: isStage,
273
+ reason: _orkestrel_contract.isNonEmptyString
274
+ });
275
+ /**
276
+ * Checks whether a value carries a project, a case, and a control.
277
+ *
278
+ * @remarks
279
+ * Exact rather than open: a claim is this package's own record, so an unknown member is a caller
280
+ * sending a contract this service does not implement rather than a wider caller it must tolerate.
281
+ *
282
+ * `CLAIM_SHAPE` is the wire contract's shape and this is the admission rule the tool enforces, and
283
+ * the rule is strictly narrower than the shape. They agree on every member but `Draft.path`: the
284
+ * shape constrains it to a non-empty string, while `isDraft` also refuses an absolute path
285
+ * and one that traverses out of the workspace, so `../../etc/hosts` satisfies
286
+ * `compileGuard(CLAIM_SHAPE)` and is refused here. `ProbeServer` advertises the shape and enforces
287
+ * this rule, so it names the refused member rather than reporting only that the claim was invalid.
288
+ *
289
+ * @param value - The value to check
290
+ * @returns True if the value is a claim; false otherwise
291
+ *
292
+ * @example
293
+ * ```ts
294
+ * isClaim({ project: 'tsconfig.json', case: subject, control }) // true
295
+ * isClaim({ project: 'tsconfig.json', case: subject }) // false
296
+ * ```
297
+ */
298
+ var isClaim = (0, _orkestrel_contract.recordOf)({
299
+ project: _orkestrel_contract.isNonEmptyString,
300
+ case: isCase,
301
+ control: isControl
302
+ });
303
+ /**
304
+ * Checks whether a value names a party an issue carries.
305
+ *
306
+ * @param value - The value to check
307
+ * @returns True if the value is a party; false otherwise
308
+ *
309
+ * @example
310
+ * ```ts
311
+ * isParty('claimant') // true
312
+ * isParty('workspace') // true
313
+ * isParty('instrument') // true
314
+ * isParty('stage') // false
315
+ * ```
316
+ */
317
+ var isParty = (0, _orkestrel_contract.literalOf)(PROBE_PARTIES);
318
+ /**
319
+ * Checks whether a value carries one message, its location, and the origin of the fault it names.
320
+ *
321
+ * @remarks
322
+ * `origin` is required rather than optional. Every issue this package produces sets it, and a
323
+ * receipt turns on it: a control's issue disproves the claim only when its origin names the
324
+ * claimant.
325
+ *
326
+ * @param value - The value to check
327
+ * @returns True if the value is an issue; false otherwise
328
+ *
329
+ * @example
330
+ * ```ts
331
+ * isIssue({ origin: 'claimant', path: 'src/core/greeting.ts', message: 'not assignable' }) // true
332
+ * isIssue({ path: 'src/core/greeting.ts', message: 'not assignable' }) // false
333
+ * ```
334
+ */
335
+ var isIssue = (0, _orkestrel_contract.recordOf)({
336
+ origin: isParty,
337
+ path: _orkestrel_contract.isString,
338
+ message: _orkestrel_contract.isString,
339
+ line: _orkestrel_contract.isNumber
340
+ }, ["line"]);
341
+ /**
342
+ * Checks whether a value carries one stage's outcome.
343
+ *
344
+ * @param value - The value to check
345
+ * @returns True if the value is a check; false otherwise
346
+ *
347
+ * @example
348
+ * ```ts
349
+ * isCheck({ stage: 'lint', elapsed: 17, issues: [] }) // true
350
+ * ```
351
+ */
352
+ var isCheck = (0, _orkestrel_contract.recordOf)({
353
+ stage: isStage,
354
+ elapsed: _orkestrel_contract.isNumber,
355
+ issues: (0, _orkestrel_contract.arrayOf)(isIssue)
356
+ });
357
+ /**
358
+ * Checks whether a value names every resolved tool version.
359
+ *
360
+ * @param value - The value to check
361
+ * @returns True if the value is a toolchain; false otherwise
362
+ *
363
+ * @example
364
+ * ```ts
365
+ * isToolchain({ typescript: '6.0.3', oxlint: '1.79.0', vitest: '4.1.11' }) // true
366
+ * isToolchain({ typescript: '6.0.3' }) // false
367
+ * ```
368
+ */
369
+ var isToolchain = (0, _orkestrel_contract.recordOf)({
370
+ typescript: _orkestrel_contract.isString,
371
+ oxlint: _orkestrel_contract.isString,
372
+ vitest: _orkestrel_contract.isString
373
+ });
374
+ /**
375
+ * Checks whether a value names one resolved TypeScript project and what it contained.
376
+ *
377
+ * @remarks
378
+ * Rejects an empty path and an empty digest. A verdict names the project that judged it, so a
379
+ * blank member here would report a project the type stage never applied.
380
+ *
381
+ * @param value - The value to check
382
+ * @returns True if the value is a project; false otherwise
383
+ *
384
+ * @example
385
+ * ```ts
386
+ * isProject({ path: 'configs/src/tsconfig.core.json', digest: '3b674fdf121c85ef' }) // true
387
+ * isProject({ path: 'configs/src/tsconfig.core.json', digest: '' }) // false
388
+ * ```
389
+ */
390
+ var isProject = (0, _orkestrel_contract.recordOf)({
391
+ path: _orkestrel_contract.isNonEmptyString,
392
+ digest: _orkestrel_contract.isNonEmptyString
393
+ });
394
+ /**
395
+ * Checks whether a value carries a complete verdict.
396
+ *
397
+ * @remarks
398
+ * Accepts any check count, because arity is the coordinator's obligation rather than this guard's:
399
+ * a stage that could not start throws before a verdict is built, so a short list can only arrive
400
+ * from a caller that assembled one by hand.
401
+ *
402
+ * @param value - The value to check
403
+ * @returns True if the value is a verdict; false otherwise
404
+ *
405
+ * @example
406
+ * ```ts
407
+ * const basis = { id: '01J8Z0', digest: '6ca20c3b', toolchain, project, elapsed: 337 }
408
+ * isVerdict({ ...basis, case: [], control: [] }) // true
409
+ * isVerdict({ ...basis, case: [] }) // false
410
+ * ```
411
+ */
412
+ var isVerdict = (0, _orkestrel_contract.recordOf)({
413
+ id: _orkestrel_contract.isString,
414
+ digest: _orkestrel_contract.isString,
415
+ toolchain: isToolchain,
416
+ project: isProject,
417
+ reason: _orkestrel_contract.isNonEmptyString,
418
+ case: (0, _orkestrel_contract.arrayOf)(isCheck),
419
+ control: (0, _orkestrel_contract.arrayOf)(isCheck),
420
+ elapsed: _orkestrel_contract.isNumber,
421
+ receipt: _orkestrel_contract.isString
422
+ }, ["reason", "receipt"]);
423
+ //#endregion
424
+ //#region src/core/helpers.ts
425
+ /**
426
+ * Renders one tool message as a single line an agent can classify and locate.
427
+ *
428
+ * @param issue - The message and location a stage reported
429
+ * @returns The bracketed origin, location, and message, separated by spaces
430
+ *
431
+ * @example
432
+ * ```ts
433
+ * const located: Issue = {
434
+ * origin: 'claimant',
435
+ * path: 'src/core/greeting.ts',
436
+ * message: 'not assignable',
437
+ * line: 1,
438
+ * }
439
+ * const whole: Issue = {
440
+ * origin: 'claimant',
441
+ * path: 'src/core/greeting.ts',
442
+ * message: 'not assignable',
443
+ * }
444
+ * formatIssue(located) // '[claimant] src/core/greeting.ts:1 not assignable'
445
+ * formatIssue(whole) // '[claimant] src/core/greeting.ts not assignable'
446
+ * ```
447
+ */
448
+ function formatIssue(issue) {
449
+ const where = issue.line === void 0 ? issue.path : `${issue.path}:${issue.line}`;
450
+ return `[${issue.origin}] ${where} ${issue.message}`;
451
+ }
452
+ /**
453
+ * Renders one stage's outcome as its summary line followed by every message it reported.
454
+ *
455
+ * @remarks
456
+ * A clean stage renders one line. The count appears even when it is zero, because that line is
457
+ * what shows a reader the stage ran; a silent stage and a skipped stage read the same.
458
+ *
459
+ * @param check - The stage outcome to render
460
+ * @returns The summary line, then one indented line per issue
461
+ *
462
+ * @example
463
+ * ```ts
464
+ * formatCheck({ stage: 'lint', elapsed: 17, issues: [] })
465
+ * // 'lint: 0 issues (17 ms)'
466
+ * ```
467
+ */
468
+ function formatCheck(check) {
469
+ const count = check.issues.length;
470
+ const noun = count === 1 ? "issue" : "issues";
471
+ return [`${check.stage}: ${count} ${noun} (${check.elapsed} ms)`, ...check.issues.map((issue) => ` ${formatIssue(issue)}`)].join("\n");
472
+ }
473
+ /**
474
+ * Renders a whole verdict as the text an agent reads.
475
+ *
476
+ * @remarks
477
+ * Every stage appears for both the case and the control, so no failing stage can be masked by an
478
+ * earlier one. The closing line always states the receipt or its absence, so a reader never has to
479
+ * infer from silence whether the claim was proven.
480
+ *
481
+ * The heading lines carry everything the receipt binds, each in the name-then-value form the
482
+ * toolchain line established: the call's identity, the claim it answered, the tools that ran, and
483
+ * the project that judged the candidates. A verdict carrying the control's reason renders it next,
484
+ * before either phase's checks. A reader comparing two verdicts therefore reads what differed
485
+ * without recomputing anything.
486
+ *
487
+ * @param verdict - The verdict to render
488
+ * @returns The identity, claim, toolchain, project, and reason when present, then every case and
489
+ * control stage, then the receipt line
490
+ *
491
+ * @example
492
+ * ```ts
493
+ * formatVerdict(verdict).split('\n')[1] // 'claim 6ca20c3bff623031d3955b9d1a76d71d'
494
+ * ```
495
+ */
496
+ function formatVerdict(verdict) {
497
+ const { typescript, oxlint, vitest } = verdict.toolchain;
498
+ const tools = `toolchain typescript ${typescript}, oxlint ${oxlint}, vitest ${vitest}`;
499
+ const project = `project ${verdict.project.path} ${verdict.project.digest}`;
500
+ const reason = verdict.reason === void 0 ? [] : [`reason ${verdict.reason}`];
501
+ const proof = verdict.receipt === void 0 ? "no receipt" : `receipt ${verdict.receipt}`;
502
+ return [
503
+ `probe ${verdict.id} (${verdict.elapsed} ms)`,
504
+ `claim ${verdict.digest}`,
505
+ tools,
506
+ project,
507
+ ...reason,
508
+ ...verdict.case.map((check) => `case ${formatCheck(check)}`),
509
+ ...verdict.control.map((check) => `control ${formatCheck(check)}`),
510
+ proof
511
+ ].join("\n");
512
+ }
513
+ /**
514
+ * Computes the proof token a verdict carries, or returns nothing when the claim was not proven.
515
+ *
516
+ * @remarks
517
+ * A receipt is minted on these conditions together: both phases report one check per stage, the
518
+ * case carries no issue, the control carries at least one `origin: 'claimant'` issue
519
+ * at the stage it declared, and every other control stage carries no claimant issue. A control
520
+ * that fails somewhere else has falsified the instrument rather than the claim, so no receipt is
521
+ * minted for it.
522
+ *
523
+ * Both phases owe every stage, not the case alone. `stayed` reads the control entries a verdict
524
+ * carries, so a control that omits a stage entirely would otherwise read as a stage that stayed
525
+ * clean, and the receipt would certify an inspection that never ran. `prove` records every stage
526
+ * for both phases, so this condition refuses only a verdict a caller assembled by hand.
527
+ *
528
+ * The token names every condition the verdict was reached under, so a receipt read away from its
529
+ * verdict still says what was judged and what judged it: the claim's digest, the stage the control
530
+ * broke at, the tool versions, and the project the candidate drafts were checked against.
531
+ * The call's identity is deliberately absent. It carries no integrity and it is the only value
532
+ * stopping two honest runs of one claim from producing one comparable string, so leaving it out is
533
+ * what makes "re-run `prove` and compare tokens" a verification a reader can perform.
534
+ *
535
+ * The project goes last, and its own path and digest are joined by `@`. Split the token on
536
+ * `RECEIPT_SEPARATOR`, take fields 0 through 5, and rejoin the remainder with that separator to
537
+ * read the project field; inside it the digest is everything after the final `@`. That rule stays
538
+ * total for a workspace-relative project path containing either character.
539
+ *
540
+ * An `origin: 'instrument'` issue in either phase means the inspection did not complete and
541
+ * refuses the receipt. A workspace issue in the control does not decide whether the claimant's
542
+ * candidate broke, while one in the case still means that the case did not run clean.
543
+ *
544
+ * @param verdict - The verdict whose case and control checks decide the outcome
545
+ * @param stage - The stage the claim's control declared it must fail at
546
+ * @returns The receipt token, or `undefined` when any condition fails
547
+ *
548
+ * @example
549
+ * ```ts
550
+ * computeReceipt(proven, 'type')
551
+ * // 'probe:6ca20c3bff623031d3955b9d1a76d71d:type:typescript@6.0.3:oxlint@1.79.0:vitest@4.1.11:configs/src/tsconfig.core.json@3b674fdf121c85efb9ed1bab25ceeec8'
552
+ * computeReceipt(proven, 'lint') // undefined
553
+ * ```
554
+ */
555
+ function computeReceipt(verdict, stage) {
556
+ const ran = PROBE_STAGES.every((name) => verdict.case.filter((check) => check.stage === name).length === 1 && verdict.control.filter((check) => check.stage === name).length === 1);
557
+ const clean = verdict.case.every((check) => check.issues.length === 0);
558
+ const broke = verdict.control.find((check) => check.stage === stage)?.issues.some((issue) => issue.origin === "claimant") ?? false;
559
+ const faulted = verdict.case.some((check) => check.issues.some((issue) => issue.origin === "instrument")) || verdict.control.some((check) => check.issues.some((issue) => issue.origin === "instrument"));
560
+ const stayed = verdict.control.every((check) => check.stage === stage || check.issues.every((issue) => issue.origin !== "claimant"));
561
+ if (!ran || !clean || !broke || faulted || !stayed) return void 0;
562
+ const { typescript, oxlint, vitest } = verdict.toolchain;
563
+ return [
564
+ RECEIPT_PREFIX,
565
+ verdict.digest,
566
+ stage,
567
+ `typescript@${typescript}`,
568
+ `oxlint@${oxlint}`,
569
+ `vitest@${vitest}`,
570
+ `${verdict.project.path}@${verdict.project.digest}`
571
+ ].join(":");
572
+ }
573
+ /**
574
+ * Renders one generated specification: the caller's own test text, then the marker naming the
575
+ * revision that wrote it.
576
+ *
577
+ * @remarks
578
+ * The marker is what makes a file in a target workspace attributable to this package. A generated
579
+ * specification carries text the caller supplied, so its name alone cannot separate it from a
580
+ * developer's own file, and a sweep reading the name alone deletes that file. The marker goes last
581
+ * so every line of the caller's text keeps the number a reported stack frame gives it, and the text
582
+ * gains a terminating newline first so the marker is never appended to a line of the test.
583
+ *
584
+ * @param text - The caller's own test text
585
+ * @param revision - The writing host's process id and a fresh UUID, joined by `-`
586
+ * @returns The bytes the runtime stage writes for that revision
587
+ *
588
+ * @example
589
+ * ```ts
590
+ * formatSpecification("test('greets', () => {})\n", '4821-9f0c')
591
+ * // "test('greets', () => {})\n// @orkestrel/probe generated specification 4821-9f0c\n"
592
+ * ```
593
+ */
594
+ function formatSpecification(text, revision) {
595
+ return `${text === "" || text.endsWith("\n") ? text : `${text}\n`}// @orkestrel/probe generated specification ${revision}\n`;
596
+ }
597
+ /**
598
+ * Checks whether one file's text is the generated specification written for one revision.
599
+ *
600
+ * @remarks
601
+ * Reads the marker `formatSpecification` writes, taken from that function rather than restated, so
602
+ * the writer and the reader cannot drift apart. An empty body renders the marker alone, which is
603
+ * why the comparison is built from a call rather than from a second copy of the text.
604
+ *
605
+ * @param text - The file's full contents
606
+ * @param revision - The revision the file's name declares
607
+ * @returns True if the text ends with this package's marker for that revision; false otherwise
608
+ *
609
+ * @example
610
+ * ```ts
611
+ * matchesSpecification(formatSpecification('', '4821-9f0c'), '4821-9f0c') // true
612
+ * matchesSpecification('export const NOTE = 1\n', '4821-9f0c') // false
613
+ * ```
614
+ */
615
+ function matchesSpecification(text, revision) {
616
+ return text.endsWith(formatSpecification("", revision));
617
+ }
618
+ //#endregion
619
+ //#region src/core/shapers.ts
620
+ /**
621
+ * Describes one proposed file a claim carries.
622
+ *
623
+ * @remarks
624
+ * `path` is constrained to a non-empty string because every stage resolves a file by it, so an
625
+ * empty path names no file. That minimum is the whole of the constraint here, and it is the one
626
+ * place the wire contract is wider than what this package admits: `isDraft` enforces the same
627
+ * minimum and also refuses an absolute path and one that traverses out of the workspace, which no
628
+ * JSON Schema keyword can express. A caller that satisfies this shape can still be refused, and
629
+ * `ProbeServer` names the member when it is.
630
+ *
631
+ * @example
632
+ * ```ts
633
+ * compileGuard(DRAFT_SHAPE)({ path: 'src/core/greeting.ts', text: '' }) // true
634
+ * ```
635
+ */
636
+ var DRAFT_SHAPE = (0, _orkestrel_contract.objectShape)({
637
+ path: (0, _orkestrel_contract.stringShape)({
638
+ min: 1,
639
+ description: "Workspace-relative path, which need not exist."
640
+ }),
641
+ text: (0, _orkestrel_contract.stringShape)({ description: "The file's full contents." })
642
+ }, { description: "One proposed file a claim carries." });
643
+ /**
644
+ * Describes the drafts a claim asserts about and the test that exercises them.
645
+ *
646
+ * @example
647
+ * ```ts
648
+ * const test = { path: 'tests/src/core/greeting.test.ts', text: '' }
649
+ * compileGuard(CASE_SHAPE)({ files: [], test }) // true
650
+ * ```
651
+ */
652
+ var CASE_SHAPE = (0, _orkestrel_contract.objectShape)({
653
+ files: (0, _orkestrel_contract.arrayShape)(DRAFT_SHAPE, { description: "The candidate drafts the test imports." }),
654
+ test: DRAFT_SHAPE
655
+ }, { description: "The drafts a claim asserts about, and the test that exercises them." });
656
+ /**
657
+ * Describes the negative control, which is a case plus where and why it must break.
658
+ *
659
+ * @remarks
660
+ * Built from `CASE_SHAPE`'s own properties rather than a second copy of them, so the control and
661
+ * the case cannot describe a draft differently.
662
+ *
663
+ * @example
664
+ * ```ts
665
+ * const test = { path: 'tests/src/core/greeting.test.ts', text: '' }
666
+ * compileGuard(CONTROL_SHAPE)({ files: [], test, stage: 'type', reason: 'must not compile' }) // true
667
+ * ```
668
+ */
669
+ var CONTROL_SHAPE = (0, _orkestrel_contract.objectShape)({
670
+ ...CASE_SHAPE.properties,
671
+ stage: (0, _orkestrel_contract.literalShape)(PROBE_STAGES, { description: "The stage this control must fail at." }),
672
+ reason: (0, _orkestrel_contract.stringShape)({
673
+ min: 1,
674
+ description: "Why the control fails at that stage."
675
+ })
676
+ }, { description: "A case that must fail, naming the stage where it must fail and why." });
677
+ /**
678
+ * Describes one claim and is the sole source of both the published tool schema and the guard
679
+ * applied to an arriving claim.
680
+ *
681
+ * @remarks
682
+ * The Model Context Protocol tool publishes `compileSchema(CLAIM_SHAPE)` and admits a call with
683
+ * `isClaim`. Deriving the advertised schema from this one value is what stops the two from drifting
684
+ * apart across a release on any member either can express. `isClaim` is narrower on the one member
685
+ * neither a schema keyword nor this shape can carry — the containment rule on `Draft.path` — and
686
+ * `validators.ts` names where.
687
+ *
688
+ * @example
689
+ * ```ts
690
+ * const schema = compileSchema(CLAIM_SHAPE)
691
+ * const admits = compileGuard(CLAIM_SHAPE)
692
+ * schema.type // 'object'
693
+ * admits({ project: 'tsconfig.json' }) // false
694
+ * ```
695
+ */
696
+ var CLAIM_SHAPE = (0, _orkestrel_contract.objectShape)({
697
+ project: (0, _orkestrel_contract.stringShape)({
698
+ min: 1,
699
+ description: "Workspace-relative TypeScript project the candidate drafts are checked against."
700
+ }),
701
+ case: CASE_SHAPE,
702
+ control: CONTROL_SHAPE
703
+ }, { description: "One claim: the project, the case, and the control that must break." });
704
+ //#endregion
705
+ exports.CASE_SHAPE = CASE_SHAPE;
706
+ exports.CLAIM_SHAPE = CLAIM_SHAPE;
707
+ exports.CONTROL_SHAPE = CONTROL_SHAPE;
708
+ exports.DRAFT_SHAPE = DRAFT_SHAPE;
709
+ exports.PROBE_ERROR_CODES = PROBE_ERROR_CODES;
710
+ exports.PROBE_PARTIES = PROBE_PARTIES;
711
+ exports.PROBE_STAGES = PROBE_STAGES;
712
+ exports.ProbeError = ProbeError;
713
+ exports.RECEIPT_PREFIX = RECEIPT_PREFIX;
714
+ exports.RECEIPT_SEPARATOR = RECEIPT_SEPARATOR;
715
+ exports.computeReceipt = computeReceipt;
716
+ exports.createDestroyedError = createDestroyedError;
717
+ exports.formatCheck = formatCheck;
718
+ exports.formatIssue = formatIssue;
719
+ exports.formatSpecification = formatSpecification;
720
+ exports.formatVerdict = formatVerdict;
721
+ exports.isCase = isCase;
722
+ exports.isCheck = isCheck;
723
+ exports.isClaim = isClaim;
724
+ exports.isControl = isControl;
725
+ exports.isDraft = isDraft;
726
+ exports.isIssue = isIssue;
727
+ exports.isParty = isParty;
728
+ exports.isProbeError = isProbeError;
729
+ exports.isProject = isProject;
730
+ exports.isStage = isStage;
731
+ exports.isToolchain = isToolchain;
732
+ exports.isVerdict = isVerdict;
733
+ exports.matchesSpecification = matchesSpecification;
734
+
735
+ //# sourceMappingURL=index.cjs.map