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