@orkestrel/brief 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,2129 @@
1
+ import { andOf, arrayOf, arrayShape, attempt, booleanShape, boundsOf, cloneJSONRecord, createContract, integerShape, isBoolean, isInteger, isNonEmptyString, isNumber, isString, literalOf, literalShape, objectShape, optionalShape, parseJSONAs, recordOf, stringShape } from "@orkestrel/contract";
2
+ import { canonicalize, collapseWhitespace, createInterpret, digestValue } from "@orkestrel/interpret";
3
+ import { atom, compound, createLogicalReasoner, createReason, formatField, logicalDefinition, rule } from "@orkestrel/reason";
4
+ import { Emitter } from "@orkestrel/emitter";
5
+ //#region src/core/constants.ts
6
+ /** The twelve `TaskOperation` values, frozen. */
7
+ var TASK_OPERATIONS = Object.freeze([
8
+ "create",
9
+ "refactor",
10
+ "debug",
11
+ "extract",
12
+ "migrate",
13
+ "explain",
14
+ "review",
15
+ "optimize",
16
+ "audit",
17
+ "test",
18
+ "document",
19
+ "plan"
20
+ ]);
21
+ /** The eight `TaskDomain` values, frozen. */
22
+ var TASK_DOMAINS = Object.freeze([
23
+ "code",
24
+ "writing",
25
+ "research",
26
+ "analysis",
27
+ "design",
28
+ "data",
29
+ "ops",
30
+ "other"
31
+ ]);
32
+ /** The five `OutputFormat` values, frozen. */
33
+ var OUTPUT_FORMATS = Object.freeze([
34
+ "markdown",
35
+ "json",
36
+ "code",
37
+ "diff",
38
+ "prose"
39
+ ]);
40
+ /** The three `RiskSeverity` values, frozen. */
41
+ var RISK_SEVERITIES = Object.freeze([
42
+ "low",
43
+ "medium",
44
+ "high"
45
+ ]);
46
+ /**
47
+ * `16` — the default turn cap `briefToGoal` renders.
48
+ *
49
+ * @remarks
50
+ * Domain-qualified so the barrel stays collision-free as sibling modules add their own
51
+ * turn defaults.
52
+ */
53
+ var DEFAULT_BRIEF_TURNS = 16;
54
+ /** `'gate'` — the id of the `gateDefinition()` logical definition. */
55
+ var GATE_ID = "gate";
56
+ /**
57
+ * Every line terminator a brief field refuses.
58
+ *
59
+ * @remarks
60
+ * The four ECMAScript line terminators, not just `\n`: a renderer that splits on any of
61
+ * them would let the other three forge a markdown row. CRLF leads the alternation so a
62
+ * Windows exemplar splits as ONE break rather than two, which would insert a blank line the
63
+ * caller never wrote. Kept unanchored and stateless — no `g` flag — so `test` never carries
64
+ * `lastIndex` between calls.
65
+ */
66
+ var LINE_BREAK_PATTERN = /\r\n|[\n\r\u2028\u2029]/;
67
+ /**
68
+ * The positive form of {@link LINE_BREAK_PATTERN}, for the shape DSL.
69
+ *
70
+ * @remarks
71
+ * `stringShape`'s `pattern` must MATCH an accepted value, so the guard's refusal regex
72
+ * cannot be reused directly. Both are derived from one character class, which is what
73
+ * keeps the hand-composed guards and the compiled shapes refusing the same strings.
74
+ */
75
+ var SINGLE_LINE_PATTERN = /^[^\n\r\u2028\u2029]*$/;
76
+ /**
77
+ * A string of one or more spaces and nothing else.
78
+ *
79
+ * @remarks
80
+ * The one exemplar side `exampleToLines` must NOT pad. CommonMark strips a fully-blank code
81
+ * span to nothing rather than one space from each end, so padding inflates an all-space value
82
+ * while every other value needs the pad to keep its own boundary spaces.
83
+ *
84
+ * `+` rather than `*`, because the EMPTY string is not that case: it has no spaces to
85
+ * preserve, and withholding the pad emitted an empty backtick run that does not close.
86
+ */
87
+ var BLANK_PATTERN = /^ +$/;
88
+ //#endregion
89
+ //#region src/core/errors.ts
90
+ /**
91
+ * The one error class this package throws.
92
+ *
93
+ * @remarks
94
+ * Throws are reserved for caller misuse: `assertBrief`, `snapshotBrief`, and `pinBrief` on
95
+ * off-contract data throw `INVALID`; any method after `destroy()` throws `DESTROYED`; and `BriefCompiler.gate` throws
96
+ * `GATE_FAILED` when a borrowed reasoner returns a non-logical result. A stage that fails
97
+ * inside `compile` is CONTAINED as a `BriefStageFailure` on the `Briefing` instead.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * import { BriefError } from '@orkestrel/brief'
102
+ *
103
+ * const error = new BriefError('INVALID', 'Brief failed the exact-record contract', {
104
+ * field: 'proofs',
105
+ * })
106
+ * error.code // 'INVALID'
107
+ * error.context // { field: 'proofs' }
108
+ * ```
109
+ */
110
+ var BriefError = class extends Error {
111
+ code;
112
+ context;
113
+ constructor(code, message, context) {
114
+ super(message);
115
+ this.name = "BriefError";
116
+ this.code = code;
117
+ if (context !== void 0) this.context = context;
118
+ }
119
+ };
120
+ /**
121
+ * Narrow a caught value to a {@link BriefError}.
122
+ *
123
+ * @param value - The caught value to inspect.
124
+ * @returns `true` when `value` is a `BriefError`.
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * import { BriefError, isBriefError } from '@orkestrel/brief'
129
+ *
130
+ * try {
131
+ * throw new BriefError('DESTROYED', 'BriefCompiler has been destroyed')
132
+ * } catch (error) {
133
+ * if (isBriefError(error)) error.code // 'DESTROYED'
134
+ * }
135
+ * ```
136
+ */
137
+ function isBriefError(value) {
138
+ return value instanceof BriefError;
139
+ }
140
+ //#endregion
141
+ //#region src/core/shapers.ts
142
+ /** A single-line string of any length, including empty. */
143
+ var textShape = stringShape({ pattern: SINGLE_LINE_PATTERN });
144
+ /** A non-empty single-line string — the shape mirror of `isLine`. */
145
+ var lineShape = stringShape({
146
+ min: 1,
147
+ pattern: SINGLE_LINE_PATTERN
148
+ });
149
+ /** The `Task` shape — closed operation and domain vocabularies plus a non-empty statement. */
150
+ var taskShape = objectShape({
151
+ operation: literalShape(TASK_OPERATIONS),
152
+ domain: literalShape(TASK_DOMAINS),
153
+ statement: lineShape
154
+ }, { description: "What the brief asks for, in one imperative sentence." });
155
+ /** The `Reference` shape — a path and the note that justifies listing it. */
156
+ var referenceShape = objectShape({
157
+ path: lineShape,
158
+ note: lineShape
159
+ }, { description: "One referenced path and why it is listed." });
160
+ /** The `Manifest` shape — four disjoint reference partitions. */
161
+ var manifestShape = objectShape({
162
+ read: arrayShape(referenceShape),
163
+ edit: arrayShape(referenceShape),
164
+ locked: arrayShape(referenceShape),
165
+ forbidden: arrayShape(referenceShape)
166
+ }, { description: "The four disjoint file partitions of a brief." });
167
+ /** The `Outcome` shape — a one-based rank, the result text, and whether it gates done. */
168
+ var outcomeShape = objectShape({
169
+ rank: integerShape({ min: 1 }),
170
+ text: lineShape,
171
+ required: booleanShape()
172
+ }, { description: "One ranked outcome — a result, never a step." });
173
+ /** The `Given` shape — one categorized context fact. */
174
+ var givenShape = objectShape({
175
+ category: lineShape,
176
+ name: lineShape,
177
+ value: textShape
178
+ }, { description: "One context fact handed to the executor." });
179
+ /** The `Example` shape — one input to output exemplar. */
180
+ var exampleShape = objectShape({
181
+ input: stringShape({ min: 1 }),
182
+ output: stringShape({ min: 1 }),
183
+ note: optionalShape(lineShape)
184
+ }, { description: "One input to output exemplar." });
185
+ /** The `Citation` shape — a name, a locator, and why the source is cited. */
186
+ var citationShape = objectShape({
187
+ name: lineShape,
188
+ url: lineShape,
189
+ note: lineShape
190
+ }, { description: "One external source; list order is the trust order." });
191
+ /** The `Gap` shape — an unknown, whether it blocks, and the candidates that would close it. */
192
+ var gapShape = objectShape({
193
+ field: lineShape,
194
+ question: lineShape,
195
+ blocking: booleanShape(),
196
+ candidates: optionalShape(arrayShape(lineShape))
197
+ }, { description: "One unresolved decision; blocking means the gate fails closed." });
198
+ /** The `Risk` shape — a closed severity, the risk, and its mitigation. */
199
+ var riskShape = objectShape({
200
+ severity: literalShape(RISK_SEVERITIES),
201
+ text: lineShape,
202
+ mitigation: lineShape
203
+ }, { description: "One pre-empted risk and the mitigation that answers it." });
204
+ /** The `Output` shape — a closed format plus its optional refinements. */
205
+ var outputShape = objectShape({
206
+ format: literalShape(OUTPUT_FORMATS),
207
+ sections: optionalShape(arrayShape(lineShape)),
208
+ include: optionalShape(arrayShape(lineShape)),
209
+ exclude: optionalShape(arrayShape(lineShape))
210
+ }, { description: "The closed shape of the deliverable." });
211
+ /** The `Proof` shape — the claim and the command that settles it. */
212
+ var proofShape = objectShape({
213
+ text: lineShape,
214
+ command: lineShape
215
+ }, { description: "One mechanical, transcript-provable check." });
216
+ /**
217
+ * The whole `Brief` shape, section shapes composed.
218
+ *
219
+ * @remarks
220
+ * `trace` and `hash` are optional because `pinBrief` fills them; an unpinned draft is
221
+ * on-contract without them.
222
+ */
223
+ var briefShape = objectShape({
224
+ task: taskShape,
225
+ authority: arrayShape(referenceShape),
226
+ manifest: manifestShape,
227
+ outcomes: arrayShape(outcomeShape),
228
+ rules: arrayShape(lineShape),
229
+ invariants: arrayShape(lineShape),
230
+ givens: arrayShape(givenShape),
231
+ examples: arrayShape(exampleShape),
232
+ assumptions: arrayShape(lineShape),
233
+ citations: arrayShape(citationShape),
234
+ gaps: arrayShape(gapShape),
235
+ risks: arrayShape(riskShape),
236
+ output: outputShape,
237
+ proofs: arrayShape(proofShape),
238
+ trace: optionalShape(lineShape),
239
+ hash: optionalShape(lineShape)
240
+ }, { description: "The closed execution contract one agent can run with no interpretation left." });
241
+ //#endregion
242
+ //#region src/core/validators.ts
243
+ /**
244
+ * `true` when the value is a string holding no line terminator, empty included.
245
+ *
246
+ * @remarks
247
+ * `briefToMarkdown` renders each brief field as ONE markdown row, so a field carrying a
248
+ * line break would forge a heading or an extra manifest row — which is how a rendered
249
+ * prompt and `briefToDispatch`'s path sets could disagree about the same brief.
250
+ */
251
+ var isText = (value) => isString(value) && !LINE_BREAK_PATTERN.test(value);
252
+ /** `true` when the value is a non-empty string holding no line terminator. */
253
+ var isLine = andOf(isNonEmptyString, isText);
254
+ /** `true` when the value is one of the twelve `TaskOperation` literals. */
255
+ var isTaskOperation = literalOf(TASK_OPERATIONS);
256
+ /** `true` when the value is one of the eight `TaskDomain` literals. */
257
+ var isTaskDomain = literalOf(TASK_DOMAINS);
258
+ /** `true` when the value is one of the five `OutputFormat` literals. */
259
+ var isOutputFormat = literalOf(OUTPUT_FORMATS);
260
+ /** `true` when the value is one of the three `RiskSeverity` literals. */
261
+ var isRiskSeverity = literalOf(RISK_SEVERITIES);
262
+ /**
263
+ * `true` when the value is a non-null object whose named members can be read.
264
+ *
265
+ * @remarks
266
+ * Wider than the contract package's plain-record guard, which refuses any object carrying its
267
+ * own prototype — a class instance among them. The verdict guards below narrow FOREIGN
268
+ * interfaces, and an
269
+ * interface is satisfied by a class instance as readily as by a literal — refusing one is the
270
+ * same narrowing-past-the-contract mistake that made an exact-record verdict guard fail the
271
+ * gate closed on a valid engine.
272
+ *
273
+ * Arrays are excluded because no interface this narrows is an array, and admitting one would
274
+ * let index access stand in for member access.
275
+ */
276
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
277
+ /**
278
+ * `true` when the value is a well-formed reasons `RuleResult`.
279
+ *
280
+ * @remarks
281
+ * `@orkestrel/reason` publishes the type but no guard for it, and `BriefCompiler` reads these
282
+ * off a BORROWED engine's return value, so the shape has to be checked rather than trusted.
283
+ */
284
+ var isRuleVerdict = (value) => isObject(value) && isNonEmptyString(value["id"]) && isBoolean(value["applied"]) && arrayOf(isBoolean)(value["premises"]) && isBoolean(value["conclusion"]);
285
+ /**
286
+ * `true` when the value is a well-formed reasons `LogicalResult`.
287
+ *
288
+ * @remarks
289
+ * The gate's reasoner is supplied by the caller through `BriefCompilerOptions.reason`, so its
290
+ * return value is FOREIGN data no matter how well-typed the interface is. `BriefCompiler`
291
+ * dereferences `reasoning`, `conclusion`, and `rules`; checking one field left a malformed
292
+ * result to throw a raw `TypeError` out of `compile`, from the very code that contains stage
293
+ * failures. Total: returns `false` for `undefined`, `null`, and every off-shape value.
294
+ *
295
+ * Checks the WHOLE published shape rather than only the three members read today, because a
296
+ * guard that narrows to `LogicalResult` while ignoring four of its members is unsound.
297
+ *
298
+ * OPEN on unknown keys, deliberately. The exact-record combinator this file uses elsewhere
299
+ * refuses a value a FOREIGN interface permits: `LogicalResult` is a TypeScript interface,
300
+ * so a conforming reasoner returning a richer result is still returning a `LogicalResult`. An
301
+ * exact check refused it and failed the gate closed on a valid engine — trading a loud crash
302
+ * for a wrong refusal, which is the worse of the two. Exactness belongs on records this
303
+ * package OWNS, where an extra key means the caller misunderstood the contract.
304
+ *
305
+ * `count` is checked as a number rather than an integer for the same reason: the published
306
+ * type says `number`, and narrowing past a foreign contract is the same mistake.
307
+ */
308
+ var isLogicalVerdict = (value) => isObject(value) && value["reasoning"] === "logical" && isBoolean(value["conclusion"]) && arrayOf(isRuleVerdict)(value["rules"]) && isNumber(value["count"]) && isBoolean(value["success"]) && arrayOf(isString)(value["trace"]) && arrayOf(isString)(value["errors"]);
309
+ /** `true` when the value is a well-formed `Task` — both vocabularies closed, statement one line. */
310
+ var isTask = recordOf({
311
+ operation: isTaskOperation,
312
+ domain: isTaskDomain,
313
+ statement: isLine
314
+ });
315
+ /** `true` when the value is a well-formed `Reference` — both members required, both single-line. */
316
+ var isReference = recordOf({
317
+ path: isLine,
318
+ note: isLine
319
+ });
320
+ /**
321
+ * `true` when the value is a well-formed `Manifest`.
322
+ *
323
+ * @remarks
324
+ * Partition presence only — disjointness is `validateBrief`'s semantic pass.
325
+ */
326
+ var isManifest = recordOf({
327
+ read: arrayOf(isReference),
328
+ edit: arrayOf(isReference),
329
+ locked: arrayOf(isReference),
330
+ forbidden: arrayOf(isReference)
331
+ });
332
+ /** `true` when the value is a well-formed `Outcome` — `rank` a positive integer. */
333
+ var isOutcome = recordOf({
334
+ rank: andOf(isInteger, boundsOf(1)),
335
+ text: isLine,
336
+ required: isBoolean
337
+ });
338
+ /** `true` when the value is a well-formed `Given` — `value` may be empty but stays one line. */
339
+ var isGiven = recordOf({
340
+ category: isLine,
341
+ name: isLine,
342
+ value: isText
343
+ });
344
+ /**
345
+ * `true` when the value is a well-formed `Example`.
346
+ *
347
+ * @remarks
348
+ * An exemplar's two sides are the ONLY members a brief lets span lines, because they
349
+ * carry code. `briefToMarkdown` fences them rather than rendering them as a row.
350
+ */
351
+ var isExample = recordOf({
352
+ input: isNonEmptyString,
353
+ output: isNonEmptyString,
354
+ note: isLine
355
+ }, ["note"]);
356
+ /** `true` when the value is a well-formed `Citation` — all three members single-line. */
357
+ var isCitation = recordOf({
358
+ name: isLine,
359
+ url: isLine,
360
+ note: isLine
361
+ });
362
+ /** `true` when the value is a well-formed `Gap`. */
363
+ var isGap = recordOf({
364
+ field: isLine,
365
+ question: isLine,
366
+ blocking: isBoolean,
367
+ candidates: arrayOf(isLine)
368
+ }, ["candidates"]);
369
+ /** `true` when the value is a well-formed `Risk` — `severity` on the closed vocabulary. */
370
+ var isRisk = recordOf({
371
+ severity: isRiskSeverity,
372
+ text: isLine,
373
+ mitigation: isLine
374
+ });
375
+ /** `true` when the value is a well-formed `Output` — `format` on the closed vocabulary. */
376
+ var isOutput = recordOf({
377
+ format: isOutputFormat,
378
+ sections: arrayOf(isLine),
379
+ include: arrayOf(isLine),
380
+ exclude: arrayOf(isLine)
381
+ }, [
382
+ "sections",
383
+ "include",
384
+ "exclude"
385
+ ]);
386
+ /** `true` when the value is a well-formed `Proof`. */
387
+ var isProof = recordOf({
388
+ text: isLine,
389
+ command: isLine
390
+ });
391
+ /**
392
+ * `true` when the value satisfies the whole exact-record `Brief` contract.
393
+ *
394
+ * @remarks
395
+ * Every section must be present; an extra key fails. `trace` and `hash` are the only
396
+ * optional members, because `pinBrief` rather than the author fills them.
397
+ */
398
+ var isBrief = recordOf({
399
+ task: isTask,
400
+ authority: arrayOf(isReference),
401
+ manifest: isManifest,
402
+ outcomes: arrayOf(isOutcome),
403
+ rules: arrayOf(isLine),
404
+ invariants: arrayOf(isLine),
405
+ givens: arrayOf(isGiven),
406
+ examples: arrayOf(isExample),
407
+ assumptions: arrayOf(isLine),
408
+ citations: arrayOf(isCitation),
409
+ gaps: arrayOf(isGap),
410
+ risks: arrayOf(isRisk),
411
+ output: isOutput,
412
+ proofs: arrayOf(isProof),
413
+ trace: isLine,
414
+ hash: isLine
415
+ }, ["trace", "hash"]);
416
+ //#endregion
417
+ //#region src/core/cloners.ts
418
+ /**
419
+ * Return a deeply owned, deeply frozen copy of a brief, refusing anything off-contract.
420
+ *
421
+ * @remarks
422
+ * The one reading boundary this package has, used by the pin, the registry, and every
423
+ * projection. It matters twice over. A brief built from caller collections ADOPTS those
424
+ * arrays, so a later `outcomes.push` would change content a hash already described. And a
425
+ * caller's object may answer differently on each read, so validating one reading and
426
+ * rendering from a second let a brief that passed the contract render a row it does not
427
+ * contain — this takes ONE reading, validates that, and freezes it.
428
+ *
429
+ * `cloneJSONRecord` is `@orkestrel/contract`'s primitive rather than the ambient
430
+ * `structuredClone`: it deep-freezes, it refuses a value JSON cannot express, and it is a
431
+ * captured import rather than a mutable global. The result is a null-prototype record, so
432
+ * compare it structurally rather than by prototype.
433
+ *
434
+ * This file imports no sibling helper, which is what lets `helpers.ts` consume it without a
435
+ * module cycle.
436
+ *
437
+ * @param source - The brief to snapshot.
438
+ * @returns A deeply frozen `Brief` sharing no reference with `source`.
439
+ * @throws {@link BriefError} `INVALID` when the value is off-contract or JSON cannot express it.
440
+ *
441
+ * @example
442
+ * ```ts
443
+ * import { brief, outcome, snapshotBrief, task } from '@orkestrel/brief'
444
+ *
445
+ * const outcomes = [outcome(1, 'shipped')]
446
+ * const owned = snapshotBrief(brief(task('plan', 'ops', 'Plan the release.'), { outcomes }))
447
+ * owned.outcomes === outcomes // false — the alias is broken
448
+ * Object.isFrozen(owned.outcomes) // true
449
+ * ```
450
+ */
451
+ function snapshotBrief(source) {
452
+ const owned = attempt(() => cloneJSONRecord(source));
453
+ if (!owned.success || !isBrief(owned.value)) throw new BriefError("INVALID", "Brief carries data that cannot be read as one value", { field: "brief" });
454
+ return owned.value;
455
+ }
456
+ //#endregion
457
+ //#region src/core/helpers.ts
458
+ /**
459
+ * Build a `Task`.
460
+ *
461
+ * @param operation - What the brief asks for, from the closed operation vocabulary.
462
+ * @param domain - The subject matter, from the closed domain vocabulary.
463
+ * @param statement - One imperative sentence naming the object of the work.
464
+ * @returns A fresh `Task`.
465
+ *
466
+ * @example
467
+ * ```ts
468
+ * import { task } from '@orkestrel/brief'
469
+ *
470
+ * task('refactor', 'code', 'Refactor useForm to native browser form APIs.')
471
+ * ```
472
+ */
473
+ function task(operation, domain, statement) {
474
+ return {
475
+ operation,
476
+ domain,
477
+ statement
478
+ };
479
+ }
480
+ /**
481
+ * Build a `Reference`.
482
+ *
483
+ * @param path - The referenced path or glob.
484
+ * @param note - Why the path is listed.
485
+ * @returns A fresh `Reference`.
486
+ *
487
+ * @example
488
+ * ```ts
489
+ * import { reference } from '@orkestrel/brief'
490
+ *
491
+ * reference('AGENTS.md', 'project law') // { path: 'AGENTS.md', note: 'project law' }
492
+ * ```
493
+ */
494
+ function reference(path, note) {
495
+ return {
496
+ path,
497
+ note
498
+ };
499
+ }
500
+ /**
501
+ * Build a `Manifest`, defaulting every absent partition to an empty list.
502
+ *
503
+ * @param partitions - The partitions to fill; a partial literal is enough.
504
+ * @returns A fresh `Manifest` with all four partitions present.
505
+ *
506
+ * @example
507
+ * ```ts
508
+ * import { manifest, reference } from '@orkestrel/brief'
509
+ *
510
+ * manifest({ edit: [reference('src/core/helpers.ts', 'implementation')] })
511
+ * ```
512
+ */
513
+ function manifest(partitions) {
514
+ return {
515
+ read: partitions?.read ?? [],
516
+ edit: partitions?.edit ?? [],
517
+ locked: partitions?.locked ?? [],
518
+ forbidden: partitions?.forbidden ?? []
519
+ };
520
+ }
521
+ /**
522
+ * Build an `Outcome`.
523
+ *
524
+ * @param rank - The one-based rank; lower ranks matter more.
525
+ * @param text - The result, never a step.
526
+ * @param required - Whether the outcome gates "done"; defaults to `true`.
527
+ * @returns A fresh `Outcome`.
528
+ *
529
+ * @example
530
+ * ```ts
531
+ * import { outcome } from '@orkestrel/brief'
532
+ *
533
+ * outcome(1, 'useForm uses native FormData with no behavior change') // required: true
534
+ * outcome(2, 'the diff stays under 200 lines', false)
535
+ * ```
536
+ */
537
+ function outcome(rank, text, required = true) {
538
+ return {
539
+ rank,
540
+ text,
541
+ required
542
+ };
543
+ }
544
+ /**
545
+ * Build a `Given`.
546
+ *
547
+ * @param category - The kind of fact — a convention, a version, a constraint.
548
+ * @param name - The fact's name.
549
+ * @param value - The fact's value, already rendered as text.
550
+ * @returns A fresh `Given`.
551
+ *
552
+ * @example
553
+ * ```ts
554
+ * import { given } from '@orkestrel/brief'
555
+ *
556
+ * given('convention', 'indentation', 'tabs')
557
+ * ```
558
+ */
559
+ function given(category, name, value) {
560
+ return {
561
+ category,
562
+ name,
563
+ value
564
+ };
565
+ }
566
+ /**
567
+ * Build an `Example`.
568
+ *
569
+ * @param input - The exemplar input.
570
+ * @param result - The expected output for that input.
571
+ * @param note - Optional detail; the key is OMITTED when absent.
572
+ * @returns A fresh `Example`.
573
+ *
574
+ * @example
575
+ * ```ts
576
+ * import { example } from '@orkestrel/brief'
577
+ *
578
+ * example('<input required>', 'validity read from el.validity')
579
+ * ```
580
+ */
581
+ function example(input, result, note) {
582
+ return note === void 0 ? {
583
+ input,
584
+ output: result
585
+ } : {
586
+ input,
587
+ output: result,
588
+ note
589
+ };
590
+ }
591
+ /**
592
+ * Build a `Citation`.
593
+ *
594
+ * @param name - The source's display name.
595
+ * @param url - Where the source lives.
596
+ * @param note - Why the source is cited.
597
+ * @returns A fresh `Citation`.
598
+ *
599
+ * @example
600
+ * ```ts
601
+ * import { citation } from '@orkestrel/brief'
602
+ *
603
+ * citation(
604
+ * 'MDN Constraint Validation',
605
+ * 'https://developer.mozilla.org/',
606
+ * 'the native validity behavior being adopted',
607
+ * )
608
+ * ```
609
+ */
610
+ function citation(name, url, note) {
611
+ return {
612
+ name,
613
+ url,
614
+ note
615
+ };
616
+ }
617
+ /**
618
+ * Build a `Gap`.
619
+ *
620
+ * @param field - The brief section the unknown belongs to.
621
+ * @param question - The question that would close it.
622
+ * @param overrides - Optional `blocking` (defaults `false`) and `candidates`; an absent
623
+ * `candidates` key is OMITTED entirely.
624
+ * @returns A fresh `Gap`.
625
+ *
626
+ * @example
627
+ * ```ts
628
+ * import { gap } from '@orkestrel/brief'
629
+ *
630
+ * gap('rules', 'Should validation message wording change?') // blocking: false
631
+ * gap('output', 'Diff or full files?', { blocking: true, candidates: ['diff', 'code'] })
632
+ * ```
633
+ */
634
+ function gap(field, question, overrides) {
635
+ const blocking = overrides?.blocking ?? false;
636
+ return overrides?.candidates === void 0 ? {
637
+ field,
638
+ question,
639
+ blocking
640
+ } : {
641
+ field,
642
+ question,
643
+ blocking,
644
+ candidates: overrides.candidates
645
+ };
646
+ }
647
+ /**
648
+ * Build a `Risk`.
649
+ *
650
+ * @param severity - The closed severity.
651
+ * @param text - What could go wrong.
652
+ * @param mitigation - What answers it.
653
+ * @returns A fresh `Risk`.
654
+ *
655
+ * @example
656
+ * ```ts
657
+ * import { risk } from '@orkestrel/brief'
658
+ *
659
+ * risk('medium', 'native validation differs subtly', 'assert message and state in tests')
660
+ * ```
661
+ */
662
+ function risk(severity, text, mitigation) {
663
+ return {
664
+ severity,
665
+ text,
666
+ mitigation
667
+ };
668
+ }
669
+ /**
670
+ * Build an `Output`.
671
+ *
672
+ * @param format - The closed deliverable format.
673
+ * @param overrides - Optional `sections` / `include` / `exclude`; absent keys are OMITTED.
674
+ * @returns A fresh `Output`.
675
+ *
676
+ * @example
677
+ * ```ts
678
+ * import { output } from '@orkestrel/brief'
679
+ *
680
+ * output('markdown') // { format: 'markdown' }
681
+ * output('diff', { include: ['updated useForm.ts'] })
682
+ * ```
683
+ */
684
+ function output(format, overrides) {
685
+ return {
686
+ format,
687
+ ...overrides?.sections === void 0 ? {} : { sections: overrides.sections },
688
+ ...overrides?.include === void 0 ? {} : { include: overrides.include },
689
+ ...overrides?.exclude === void 0 ? {} : { exclude: overrides.exclude }
690
+ };
691
+ }
692
+ /**
693
+ * Build a `Proof`.
694
+ *
695
+ * @param text - What the check settles.
696
+ * @param command - The command whose exit signal settles it.
697
+ * @returns A fresh `Proof`.
698
+ *
699
+ * @example
700
+ * ```ts
701
+ * import { proof } from '@orkestrel/brief'
702
+ *
703
+ * proof('type-check and lint pass', 'npm run check')
704
+ * ```
705
+ */
706
+ function proof(text, command) {
707
+ return {
708
+ text,
709
+ command
710
+ };
711
+ }
712
+ /**
713
+ * Build a `Brief` from a `Task` plus section overrides.
714
+ *
715
+ * @param subject - The task the brief is about.
716
+ * @param overrides - Any sections to fill; every absent collection defaults to `[]`,
717
+ * `output` defaults to `output('markdown')`, and `trace` / `hash` stay OMITTED so
718
+ * `pinBrief` can fill them.
719
+ * @returns A fresh, unpinned `Brief`.
720
+ *
721
+ * @example
722
+ * ```ts
723
+ * import { brief, outcome, proof, task } from '@orkestrel/brief'
724
+ *
725
+ * brief(task('audit', 'code', 'Audit the barrel for undocumented exports.'), {
726
+ * outcomes: [outcome(1, 'every export appears in the guide')],
727
+ * proofs: [proof('parity passes', 'npm run test:guides')],
728
+ * })
729
+ * ```
730
+ */
731
+ function brief(subject, overrides) {
732
+ return {
733
+ task: subject,
734
+ authority: overrides?.authority ?? [],
735
+ manifest: overrides?.manifest ?? manifest(),
736
+ outcomes: overrides?.outcomes ?? [],
737
+ rules: overrides?.rules ?? [],
738
+ invariants: overrides?.invariants ?? [],
739
+ givens: overrides?.givens ?? [],
740
+ examples: overrides?.examples ?? [],
741
+ assumptions: overrides?.assumptions ?? [],
742
+ citations: overrides?.citations ?? [],
743
+ gaps: overrides?.gaps ?? [],
744
+ risks: overrides?.risks ?? [],
745
+ output: overrides?.output ?? output("markdown"),
746
+ proofs: overrides?.proofs ?? []
747
+ };
748
+ }
749
+ /**
750
+ * Build the fail-closed readiness gate as a reasons `LogicalDefinition`.
751
+ *
752
+ * @remarks
753
+ * Six readiness rules each derive one named fact from `briefToSubject`'s measures, and a
754
+ * final `ready` rule conjoins all six. Forward chaining reports the LAST rule's
755
+ * conclusion, so `LogicalResult.conclusion` is exactly `ready`.
756
+ *
757
+ * The gate takes NO parameters, and that is deliberate rather than unfinished. The
758
+ * reasoner overlays every derived fact into one flat namespace, so a caller rule named
759
+ * for a readiness fact overwrites it and `ready` then conjoins a fact no base rule
760
+ * proved — a refusal silently becomes a pass. Readiness is this package's contract, not
761
+ * a caller setting. A caller who needs different readiness composes their own
762
+ * `LogicalDefinition` over `briefToSubject` and evaluates it on their own reasoner; both
763
+ * are exported for exactly that, and neither can reach this definition.
764
+ *
765
+ * @returns A fresh `LogicalDefinition` with id `GATE_ID`.
766
+ *
767
+ * @example
768
+ * ```ts
769
+ * import { briefToSubject, gateDefinition } from '@orkestrel/brief'
770
+ * import { createLogicalReasoner, createReason } from '@orkestrel/reason'
771
+ *
772
+ * const reason = createReason({ reasoners: [createLogicalReasoner()] })
773
+ * const verdict = reason.reason(briefToSubject(pinned), gateDefinition())
774
+ * reason.destroy()
775
+ * ```
776
+ */
777
+ function gateDefinition() {
778
+ const readiness = [
779
+ rule("specified", [atom("blocking", "equals", 0)], atom("specified", "equals", true)),
780
+ rule("aimed", [compound("and", [atom("outcomes", "above", 0), atom("required", "above", 0)])], atom("aimed", "equals", true)),
781
+ rule("proven", [atom("proofs", "above", 0)], atom("proven", "equals", true)),
782
+ rule("disjoint", [atom("overlaps", "equals", 0)], atom("disjoint", "equals", true)),
783
+ rule("granted", [atom("ungranted", "equals", 0)], atom("granted", "equals", true)),
784
+ rule("single", [atom("sentences", "equals", 1)], atom("single", "equals", true))
785
+ ];
786
+ return logicalDefinition(GATE_ID, "Brief readiness", [...readiness, rule("ready", [compound("and", readiness.map((entry) => atom(entry.id, "equals", true)))], atom("ready", "equals", true))]);
787
+ }
788
+ /**
789
+ * The readiness rules a brief fails, computed directly from its own measures.
790
+ *
791
+ * @remarks
792
+ * The gate's decision, in code. `gateDefinition()` states the same six rules as data for a
793
+ * reasoner to narrate, and a narration is not a decision: `BriefCompilerOptions.reason` lets a
794
+ * caller supply the engine, and an engine that answers "met" to everything would otherwise
795
+ * emit a brief with no proofs. `compile` refuses on THIS and keeps the verdict for its
796
+ * trace, so a supplied engine can add detail and never remove a refusal.
797
+ *
798
+ * The two must agree. `tests/src/core/helpers.test.ts` drives both over one value set, which
799
+ * is what stops the data and the code from drifting apart.
800
+ *
801
+ * @param source - The brief to measure.
802
+ * @returns The unmet rule ids, in gate order; empty when the brief is ready.
803
+ *
804
+ * @example
805
+ * ```ts
806
+ * import { brief, findUnmetRules, outcome, proof, task } from '@orkestrel/brief'
807
+ *
808
+ * findUnmetRules(brief(task('plan', 'ops', 'Plan the release.'))) // ['aimed', 'proven']
809
+ * findUnmetRules(
810
+ * brief(task('plan', 'ops', 'Plan the release.'), {
811
+ * outcomes: [outcome(1, 'shipped')],
812
+ * proofs: [proof('x', 'npm test')],
813
+ * }),
814
+ * ) // []
815
+ * ```
816
+ */
817
+ function findUnmetRules(source) {
818
+ const unready = [];
819
+ if (findBlockingGaps(source).length !== 0) unready.push("specified");
820
+ if (source.outcomes.length === 0 || source.outcomes.filter((entry) => entry.required).length === 0) unready.push("aimed");
821
+ if (source.proofs.length === 0) unready.push("proven");
822
+ if (findManifestOverlaps(source).length !== 0) unready.push("disjoint");
823
+ if (findUngrantedAuthority(source).length !== 0) unready.push("granted");
824
+ if (countSentences(source.task.statement) !== 1) unready.push("single");
825
+ return unready;
826
+ }
827
+ /**
828
+ * Count the sentences a statement holds.
829
+ *
830
+ * @remarks
831
+ * A terminator run (`.`, `!`, `?`) followed by whitespace or the end of the text closes one
832
+ * sentence, and a trailing run with no terminator closes one more.
833
+ *
834
+ * LIMIT, stated because it decides a gate: an embedded abbreviation reads as a boundary, so
835
+ * `'Ask Dr. Smith'` and `'Compare React vs. Vue'` count TWO and the `single` rule refuses
836
+ * them. Rewrite the statement without the abbreviation — a brief's statement is one
837
+ * imperative sentence naming the object of the work, and it rarely needs one.
838
+ *
839
+ * This is inherent rather than unfinished. Separating `'Dr.'` from a real boundary needs a
840
+ * lexicon or a heuristic over capitalisation and word length, and a heuristic gets a
841
+ * different set of statements wrong — quietly, in the direction of letting a genuinely
842
+ * compound statement through, which is the failure this rule exists to prevent. `validateBrief`
843
+ * therefore reports the count and lets the author judge, rather than guessing at intent.
844
+ *
845
+ * @param statement - The statement to measure.
846
+ * @returns The sentence count; `0` for empty or whitespace-only text.
847
+ *
848
+ * @example
849
+ * ```ts
850
+ * import { countSentences } from '@orkestrel/brief'
851
+ *
852
+ * countSentences('Refactor useForm to native APIs.') // 1
853
+ * countSentences('Refactor useForm. Then update the tests') // 2 — the tail counts
854
+ * countSentences('Ask Dr. Smith') // 2 — an abbreviation reads as a boundary
855
+ * countSentences('') // 0
856
+ * ```
857
+ */
858
+ function countSentences(statement) {
859
+ const text = collapseWhitespace(statement);
860
+ if (text.length === 0) return 0;
861
+ const matches = text.match(/[.!?]+(?=\s|$)/gu);
862
+ if (matches === null) return 1;
863
+ return /[.!?]$/u.test(text) ? matches.length : matches.length + 1;
864
+ }
865
+ /**
866
+ * The gaps that block emission.
867
+ *
868
+ * @param source - The brief to inspect.
869
+ * @returns Every gap carrying `blocking: true`, in declaration order.
870
+ *
871
+ * @example
872
+ * ```ts
873
+ * import { brief, findBlockingGaps, gap, task } from '@orkestrel/brief'
874
+ *
875
+ * const draft = brief(task('plan', 'ops', 'Plan the release.'), {
876
+ * gaps: [gap('output', 'Diff or files?', { blocking: true })],
877
+ * })
878
+ * findBlockingGaps(draft).length // 1
879
+ * ```
880
+ */
881
+ function findBlockingGaps(source) {
882
+ return source.gaps.filter((entry) => entry.blocking);
883
+ }
884
+ /**
885
+ * The authority paths the manifest never grants access to.
886
+ *
887
+ * @remarks
888
+ * An authority the executor cannot open is an instruction it cannot follow, so every ranked
889
+ * path must appear in `read`, `edit`, or `locked`. Those three are the grants: `locked` is a
890
+ * grant, because read-only is exactly what obeying a file requires.
891
+ *
892
+ * This subsumes the narrower question of an authority sitting in `forbidden`. The four
893
+ * partitions are disjoint — `findManifestOverlaps` and the `disjoint` rule enforce it — so a
894
+ * forbidden path is in none of the three grants and is reported here. An authority named in
895
+ * NO partition at all is reported for the same reason, and that is the case a forbidden-only
896
+ * check misses entirely: the brief simply never says the executor may open what it must obey.
897
+ *
898
+ * Paths are compared as EXACT strings, matching `findManifestOverlaps`. A glob is never
899
+ * expanded, so `read: 'guides/**'` does not grant `authority: 'guides/brief.md'`. State a
900
+ * grant as the same literal path the authority carries.
901
+ *
902
+ * @param source - The brief to inspect.
903
+ * @returns Each ungranted authority path once, in authority order; empty when all are granted.
904
+ *
905
+ * @example
906
+ * ```ts
907
+ * import { brief, findUngrantedAuthority, manifest, reference, task } from '@orkestrel/brief'
908
+ *
909
+ * const draft = brief(task('debug', 'code', 'Fix the leak.'), {
910
+ * authority: [reference('AGENTS.md', 'project law')],
911
+ * manifest: manifest(),
912
+ * })
913
+ * findUngrantedAuthority(draft) // ['AGENTS.md'] — ranked, but no partition opens it
914
+ * ```
915
+ */
916
+ function findUngrantedAuthority(source) {
917
+ const granted = new Set([
918
+ ...source.manifest.read,
919
+ ...source.manifest.edit,
920
+ ...source.manifest.locked
921
+ ].map((entry) => entry.path));
922
+ const ungranted = [];
923
+ for (const path of new Set(source.authority.map((entry) => entry.path))) if (!granted.has(path)) ungranted.push(path);
924
+ return ungranted;
925
+ }
926
+ /**
927
+ * The paths appearing in more than one manifest partition.
928
+ *
929
+ * @remarks
930
+ * Duplicates WITHIN one partition are not an overlap; the four partitions must be
931
+ * mutually disjoint, which is what `validateBrief` errors on.
932
+ *
933
+ * Paths are compared as EXACT strings. A glob is never expanded, so `edit: 'app/file.ts'`
934
+ * and `forbidden: 'app/**'` are not reported as an overlap even though a walker would place
935
+ * one inside the other. Disjointness here is a property of the written paths.
936
+ *
937
+ * @param source - The brief to inspect.
938
+ * @returns Each overlapping path once, in first-seen partition order.
939
+ *
940
+ * @example
941
+ * ```ts
942
+ * import { brief, findManifestOverlaps, manifest, reference, task } from '@orkestrel/brief'
943
+ *
944
+ * const draft = brief(task('debug', 'code', 'Fix the leak.'), {
945
+ * manifest: manifest({
946
+ * edit: [reference('src/core/BriefCompiler.ts', 'the leaking pipeline')],
947
+ * locked: [reference('src/core/BriefCompiler.ts', 'the published contract')],
948
+ * }),
949
+ * })
950
+ * findManifestOverlaps(draft) // ['src/core/BriefCompiler.ts']
951
+ * ```
952
+ */
953
+ function findManifestOverlaps(source) {
954
+ const counts = /* @__PURE__ */ new Map();
955
+ const partitions = [
956
+ source.manifest.read,
957
+ source.manifest.edit,
958
+ source.manifest.locked,
959
+ source.manifest.forbidden
960
+ ];
961
+ for (const partition of partitions) for (const path of new Set(partition.map((entry) => entry.path))) counts.set(path, (counts.get(path) ?? 0) + 1);
962
+ const overlaps = [];
963
+ for (const [path, count] of counts) if (count > 1) overlaps.push(path);
964
+ return overlaps;
965
+ }
966
+ /**
967
+ * The open gaps with no assumption to stand on.
968
+ *
969
+ * @remarks
970
+ * The discipline is exactly one recorded assumption per open gap, so the open gaps past
971
+ * the assumption count are the unpaired ones. A blocking gap is never unpaired — it is
972
+ * a question, not something to assume around.
973
+ *
974
+ * @param source - The brief to inspect.
975
+ * @returns The surplus open gaps, in declaration order.
976
+ *
977
+ * @example
978
+ * ```ts
979
+ * import { brief, findUnpairedGaps, gap, task } from '@orkestrel/brief'
980
+ *
981
+ * const draft = brief(task('plan', 'ops', 'Plan the release.'), {
982
+ * gaps: [gap('rules', 'Keep the wording?'), gap('output', 'Diff or files?')],
983
+ * assumptions: ['Wording is preserved.'],
984
+ * })
985
+ * findUnpairedGaps(draft).length // 1
986
+ * ```
987
+ */
988
+ function findUnpairedGaps(source) {
989
+ return source.gaps.filter((entry) => !entry.blocking).slice(source.assumptions.length);
990
+ }
991
+ /**
992
+ * Project a brief into the reasons `Subject` of readiness measures the gate reads.
993
+ *
994
+ * @param source - The brief to measure.
995
+ * @returns A flat record of counts plus the task's two vocabulary values.
996
+ *
997
+ * @example
998
+ * ```ts
999
+ * import { brief, briefToSubject, proof, task } from '@orkestrel/brief'
1000
+ *
1001
+ * briefToSubject(brief(task('test', 'code', 'Cover the gate.'), { proofs: [proof('x', 'y')] }))
1002
+ * // { operation: 'test', domain: 'code', sentences: 1, proofs: 1, … }
1003
+ * ```
1004
+ */
1005
+ function briefToSubject(source) {
1006
+ return {
1007
+ operation: source.task.operation,
1008
+ domain: source.task.domain,
1009
+ sentences: countSentences(source.task.statement),
1010
+ authority: source.authority.length,
1011
+ gaps: source.gaps.length,
1012
+ blocking: findBlockingGaps(source).length,
1013
+ unpaired: findUnpairedGaps(source).length,
1014
+ outcomes: source.outcomes.length,
1015
+ required: source.outcomes.filter((entry) => entry.required).length,
1016
+ proofs: source.proofs.length,
1017
+ reads: source.manifest.read.length,
1018
+ edits: source.manifest.edit.length,
1019
+ locks: source.manifest.locked.length,
1020
+ bans: source.manifest.forbidden.length,
1021
+ overlaps: findManifestOverlaps(source).length,
1022
+ ungranted: findUngrantedAuthority(source).length,
1023
+ risks: source.risks.length,
1024
+ examples: source.examples.length
1025
+ };
1026
+ }
1027
+ /**
1028
+ * The semantic pass over an already-shape-valid brief.
1029
+ *
1030
+ * @remarks
1031
+ * ERRORS are the structural violations no assumption can paper over: a manifest
1032
+ * overlap, an authority no partition grants access to, an empty `proofs` list, and a
1033
+ * statement that is not exactly one sentence.
1034
+ * WARNINGS are runnable but suspicious: duplicate outcome ranks, an unpaired open gap,
1035
+ * and an optional outcome ranked above a required one. Never throws.
1036
+ *
1037
+ * @param source - The brief to inspect.
1038
+ * @returns A reasons `ReasonValidationResult`; `valid` exactly when `errors` is empty.
1039
+ *
1040
+ * @example
1041
+ * ```ts
1042
+ * import { brief, proof, task, validateBrief } from '@orkestrel/brief'
1043
+ *
1044
+ * validateBrief(brief(task('plan', 'ops', 'Plan the release.'))) // valid: false — no proofs
1045
+ * validateBrief(
1046
+ * brief(task('plan', 'ops', 'Plan the release.'), { proofs: [proof('ok', 'npm test')] }),
1047
+ * ) // valid: true
1048
+ * ```
1049
+ */
1050
+ function validateBrief(source) {
1051
+ const errors = [];
1052
+ const warnings = [];
1053
+ for (const path of findManifestOverlaps(source)) errors.push(`Path "${path}" appears in more than one manifest partition`);
1054
+ for (const path of findUngrantedAuthority(source)) errors.push(`Authority "${path}" is in no manifest partition that grants access — the executor cannot obey what it cannot open`);
1055
+ if (source.proofs.length === 0) errors.push("Brief records no proof — nothing can settle \"done\"");
1056
+ const sentences = countSentences(source.task.statement);
1057
+ if (sentences !== 1) errors.push(`Statement holds ${String(sentences)} sentences — a compound statement is two briefs`);
1058
+ const ranks = /* @__PURE__ */ new Map();
1059
+ for (const entry of source.outcomes) ranks.set(entry.rank, (ranks.get(entry.rank) ?? 0) + 1);
1060
+ for (const [rank, count] of ranks) if (count > 1) warnings.push(`Outcome rank ${String(rank)} is used ${String(count)} times`);
1061
+ for (const entry of findUnpairedGaps(source)) warnings.push(`Open gap "${entry.field}" has no paired assumption`);
1062
+ const required = source.outcomes.filter((entry) => entry.required).map((entry) => entry.rank);
1063
+ if (required.length > 0) {
1064
+ const floor = Math.min(...required);
1065
+ for (const entry of source.outcomes) if (!entry.required && entry.rank < floor) warnings.push(`Outcome ${String(entry.rank)} is optional but outranks every required outcome`);
1066
+ }
1067
+ return {
1068
+ valid: errors.length === 0,
1069
+ errors,
1070
+ warnings
1071
+ };
1072
+ }
1073
+ /**
1074
+ * The canonical structural digest of a brief's content.
1075
+ *
1076
+ * @remarks
1077
+ * `trace` and `hash` are stripped before digesting, so the value is the identity of what
1078
+ * the brief SAYS rather than of a particular pinning. Deterministic across runs — the
1079
+ * same interprets `digestValue` the fleet uses everywhere else.
1080
+ *
1081
+ * @param source - The brief to digest.
1082
+ * @returns An eight-hex-digit digest.
1083
+ *
1084
+ * @example
1085
+ * ```ts
1086
+ * import { brief, briefToHash, pinBrief, task } from '@orkestrel/brief'
1087
+ *
1088
+ * const draft = brief(task('plan', 'ops', 'Plan the release.'))
1089
+ * briefToHash(draft) === briefToHash(pinBrief(draft)) // true — pinning does not move it
1090
+ * ```
1091
+ */
1092
+ function briefToHash(source) {
1093
+ return digestValue(briefToContent(source));
1094
+ }
1095
+ /**
1096
+ * The canonical text of exactly what a brief's hash describes.
1097
+ *
1098
+ * @remarks
1099
+ * `trace` and `hash` are stripped, then interprets `canonicalize` renders the rest in a
1100
+ * key-order-stable form. Two briefs with the same hash are the same brief only when this
1101
+ * text matches — the digest is eight hex digits, so hash equality alone is not identity.
1102
+ *
1103
+ * @param source - The brief to render.
1104
+ * @returns The canonical content text.
1105
+ *
1106
+ * @example
1107
+ * ```ts
1108
+ * import { brief, briefToContent, pinBrief, task } from '@orkestrel/brief'
1109
+ *
1110
+ * const draft = brief(task('plan', 'ops', 'Plan the release.'))
1111
+ * briefToContent(draft) === briefToContent(pinBrief(draft)) // true — pinning adds no content
1112
+ * ```
1113
+ */
1114
+ function briefToContent(source) {
1115
+ const { trace: _trace, hash: _hash, ...content } = source;
1116
+ return canonicalize(content);
1117
+ }
1118
+ /**
1119
+ * Freeze a value and everything reachable from it.
1120
+ *
1121
+ * @remarks
1122
+ * `Object.freeze` is SHALLOW, so freezing a record leaves every nested array and object
1123
+ * writable. A `Briefing` is documented as a replayable record, and a shallow freeze let a
1124
+ * consumer rewrite the recorded stage input after the digest describing it was already
1125
+ * sealed — the replay and its hash could disagree.
1126
+ *
1127
+ * Cycles terminate: `structuredClone` preserves them, so a naive walk would not return.
1128
+ * Delegates each branch to `freezeBranch` with the shared visited set.
1129
+ *
1130
+ * Reaches PLAIN objects and arrays, which is the whole of a `Brief` — it is JSON-serializable
1131
+ * by contract. A `Map`, `Set`, or typed array is frozen as an object and its CONTENTS are left
1132
+ * writable, and `Object.isFrozen` reports `true` for it either way. Nothing this package
1133
+ * produces contains one; a caller freezing their own value should know the limit.
1134
+ *
1135
+ * @param value - The value to freeze in place; returned for convenience.
1136
+ * @returns The same value, now deeply frozen.
1137
+ *
1138
+ * @example
1139
+ * ```ts
1140
+ * import { freezeDeep } from '@orkestrel/brief'
1141
+ *
1142
+ * const owned = freezeDeep({ outcomes: [{ rank: 1 }] })
1143
+ * Object.isFrozen(owned.outcomes) // true — the nested array too
1144
+ * ```
1145
+ */
1146
+ function freezeDeep(value) {
1147
+ return freezeBranch(value, /* @__PURE__ */ new WeakSet());
1148
+ }
1149
+ /**
1150
+ * Freeze one branch of a value graph, skipping what the visited set already holds.
1151
+ *
1152
+ * @param value - The branch to freeze.
1153
+ * @param seen - The objects already frozen on this walk; what makes a cycle terminate.
1154
+ * @returns The same branch, now frozen.
1155
+ *
1156
+ * @example
1157
+ * ```ts
1158
+ * import { freezeBranch } from '@orkestrel/brief'
1159
+ *
1160
+ * freezeBranch({ a: [1] }, new WeakSet()) // frozen, one level of nesting included
1161
+ * ```
1162
+ */
1163
+ function freezeBranch(value, seen) {
1164
+ if (value === null || typeof value !== "object") return value;
1165
+ if (seen.has(value)) return value;
1166
+ seen.add(value);
1167
+ Object.freeze(value);
1168
+ for (const nested of Object.values(value)) freezeBranch(nested, seen);
1169
+ return value;
1170
+ }
1171
+ /**
1172
+ * Render a value thrown by a stage into a message.
1173
+ *
1174
+ * @remarks
1175
+ * TOTAL: it never throws, for any input. That is load-bearing rather than tidy, because this
1176
+ * is the containment code itself — `compile` calls it inside the `catch` that turns a thrown
1177
+ * stage into a recorded `BriefStageFailure`. A throw here escapes `compile` uncontained and
1178
+ * falsifies the package's central promise that a failing stage yields an incomplete
1179
+ * `Briefing` rather than an exception.
1180
+ *
1181
+ * Three real inputs used to throw: an `Error` subclass whose `message` getter throws, a value
1182
+ * whose string conversion throws, and a null-prototype object, which has no inherited
1183
+ * conversion for String() to reach. Each is wrapped, and an unreadable value degrades to its
1184
+ * type rather than propagating.
1185
+ *
1186
+ * @param error - The caught value, of any shape.
1187
+ * @returns The `Error` message when there is one, otherwise the value stringified; a fixed
1188
+ * description when the value cannot be read at all.
1189
+ *
1190
+ * @example
1191
+ * ```ts
1192
+ * import { errorToMessage } from '@orkestrel/brief'
1193
+ *
1194
+ * errorToMessage(new Error('boom')) // 'boom'
1195
+ * errorToMessage('boom') // 'boom'
1196
+ * errorToMessage(Object.create(null)) // 'an unreadable object was thrown'
1197
+ * ```
1198
+ */
1199
+ function errorToMessage(error) {
1200
+ const read = attempt(() => error instanceof Error ? error.message : String(error));
1201
+ if (read.success && typeof read.value === "string") return read.value;
1202
+ return `an unreadable ${typeof error} was thrown`;
1203
+ }
1204
+ /**
1205
+ * Narrow unknown data to a `Brief`, throwing when it is off-contract.
1206
+ *
1207
+ * @remarks
1208
+ * The throwing half of the intake pair: this returns its argument by IDENTITY once the
1209
+ * guard passes, while `parseBrief` returns `undefined` for bad input. It constructs
1210
+ * nothing, so it is an assertion rather than a factory. Reserve it for programmer-error
1211
+ * contexts where invalidity is a bug.
1212
+ *
1213
+ * @param data - The candidate brief data.
1214
+ * @returns The same value, now known to satisfy {@link Brief}.
1215
+ * @throws {@link BriefError} `INVALID` when `data` fails `isBrief`.
1216
+ *
1217
+ * @example
1218
+ * ```ts
1219
+ * import { assertBrief, brief, proof, task } from '@orkestrel/brief'
1220
+ *
1221
+ * assertBrief(brief(task('plan', 'ops', 'Plan the release.'), { proofs: [proof('x', 'y')] }))
1222
+ * assertBrief({ task: { operation: 'plan', domain: 'ops', statement: 'x.' } }) // throws INVALID
1223
+ * ```
1224
+ */
1225
+ function assertBrief(data) {
1226
+ if (!isBrief(data)) throw new BriefError("INVALID", "Brief failed the exact-record contract", { field: "brief" });
1227
+ return data;
1228
+ }
1229
+ /**
1230
+ * Return a fresh brief with `trace` and `hash` derived from its own content.
1231
+ *
1232
+ * @remarks
1233
+ * Deterministic: no clock, no randomness, no run-specific data. Any existing `trace` /
1234
+ * `hash` is stripped before the digest, so pinning is idempotent and a re-pin of unchanged
1235
+ * content produces the same hash.
1236
+ *
1237
+ * The snapshot is taken FIRST, before any member is read, so a hostile input whose getters
1238
+ * throw surfaces as this package's coded error rather than as whatever it threw.
1239
+ *
1240
+ * @param source - The brief to pin.
1241
+ * @returns A fresh, pinned, deeply frozen `Brief`.
1242
+ * @throws {@link BriefError} `INVALID` when the brief carries data JSON cannot express.
1243
+ *
1244
+ * @example
1245
+ * ```ts
1246
+ * import { brief, pinBrief, task } from '@orkestrel/brief'
1247
+ *
1248
+ * const pinned = pinBrief(brief(task('document', 'writing', 'Write the brief guide.')))
1249
+ * pinned.hash // an 8-hex-digit structural digest
1250
+ * pinned.trace // 'document/writing · outcomes:0 · gaps:0/0 · proofs:0'
1251
+ * ```
1252
+ */
1253
+ function pinBrief(source) {
1254
+ const owned = snapshotBrief(source);
1255
+ const { trace: _trace, hash: _hash, ...content } = owned;
1256
+ return snapshotBrief({
1257
+ ...content,
1258
+ trace: briefToTrace(owned),
1259
+ hash: briefToHash(owned)
1260
+ });
1261
+ }
1262
+ /**
1263
+ * The one-line census `pinBrief` stamps onto a brief.
1264
+ *
1265
+ * @remarks
1266
+ * Extracted so it has ONE implementation. `pinBrief` derives it and `BriefManager` re-derives
1267
+ * it to reconcile an inbound brief's own `trace` against its content — an inbound `trace` is
1268
+ * shape-checked rather than verified, and it is the line `briefToMarkdown` prints at the top
1269
+ * of the executor's prompt, so a stale one misdescribes the brief where it is most read.
1270
+ *
1271
+ * @param source - The brief to describe.
1272
+ * @returns The census line: operation/domain, outcomes, blocking-over-total gaps, proofs.
1273
+ *
1274
+ * @example
1275
+ * ```ts
1276
+ * import { brief, briefToTrace, task } from '@orkestrel/brief'
1277
+ *
1278
+ * briefToTrace(brief(task('document', 'writing', 'Write the guide.')))
1279
+ * // 'document/writing · outcomes:0 · gaps:0/0 · proofs:0'
1280
+ * ```
1281
+ */
1282
+ function briefToTrace(source) {
1283
+ return [
1284
+ `${source.task.operation}/${source.task.domain}`,
1285
+ `outcomes:${String(source.outcomes.length)}`,
1286
+ `gaps:${String(findBlockingGaps(source).length)}/${String(source.gaps.length)}`,
1287
+ `proofs:${String(source.proofs.length)}`
1288
+ ].join(" · ");
1289
+ }
1290
+ /**
1291
+ * Render one exemplar as markdown lines.
1292
+ *
1293
+ * @remarks
1294
+ * An `Example`'s two sides are the only brief members permitted to span lines, so a
1295
+ * single-line pair renders as one row and a multi-line pair renders as a fenced block.
1296
+ * Fencing is what stops the one permissive field from forging a heading.
1297
+ *
1298
+ * @param entry - The exemplar to render.
1299
+ * @returns The markdown lines, without a trailing blank.
1300
+ *
1301
+ * @example
1302
+ * ```ts
1303
+ * import { example, exampleToLines } from '@orkestrel/brief'
1304
+ *
1305
+ * exampleToLines(example('<input required>', 'el.validity')) // ['- ` <input required> ` → ` el.validity `']
1306
+ * ```
1307
+ */
1308
+ function exampleToLines(entry) {
1309
+ const note = entry.note === void 0 ? "" : ` (${entry.note})`;
1310
+ let runs = 0;
1311
+ let current = 0;
1312
+ for (const character of `${entry.input} ${entry.output}`) {
1313
+ current = character === "`" ? current + 1 : 0;
1314
+ if (current > runs) runs = current;
1315
+ }
1316
+ if (!LINE_BREAK_PATTERN.test(entry.input) && !LINE_BREAK_PATTERN.test(entry.output)) {
1317
+ const tick = "`".repeat(runs + 1);
1318
+ const inputPad = BLANK_PATTERN.test(entry.input) ? "" : " ";
1319
+ const outputPad = BLANK_PATTERN.test(entry.output) ? "" : " ";
1320
+ return [`- ${tick}${inputPad}${entry.input}${inputPad}${tick} → ${tick}${outputPad}${entry.output}${outputPad}${tick}${note}`];
1321
+ }
1322
+ const fence = "`".repeat(Math.max(3, runs) + 1);
1323
+ return [
1324
+ `- exemplar${note}`,
1325
+ "",
1326
+ ` ${fence}text`,
1327
+ ...entry.input.split(LINE_BREAK_PATTERN).map((line) => ` ${line}`),
1328
+ ` ${fence}`,
1329
+ "",
1330
+ ` ${fence}text`,
1331
+ ...entry.output.split(LINE_BREAK_PATTERN).map((line) => ` ${line}`),
1332
+ ` ${fence}`
1333
+ ];
1334
+ }
1335
+ /**
1336
+ * Project a brief into the copy-ready agent prompt.
1337
+ *
1338
+ * @remarks
1339
+ * Paths are REFERENCED, never inlined — the executor retrieves them. An empty section is
1340
+ * omitted entirely, so the rendering carries no filler an executor must read past.
1341
+ *
1342
+ * @param source - The brief to render.
1343
+ * @returns The markdown prompt.
1344
+ *
1345
+ * @example
1346
+ * ```ts
1347
+ * import { brief, briefToMarkdown, task } from '@orkestrel/brief'
1348
+ *
1349
+ * briefToMarkdown(brief(task('review', 'code', 'Review the gate rules.')))
1350
+ * // '# Brief: Review the gate rules.\n\nreview · code\n\n## Output\n\n- format: markdown\n'
1351
+ * ```
1352
+ */
1353
+ function briefToMarkdown(input) {
1354
+ const source = snapshotBrief(input);
1355
+ const lines = [`# Brief: ${source.task.statement}`, ""];
1356
+ lines.push(`${source.task.operation} · ${source.task.domain}`, "");
1357
+ if (source.trace !== void 0) lines.push(`Trace: ${source.trace}`, "");
1358
+ if (source.hash !== void 0) lines.push(`Hash: ${source.hash}`, "");
1359
+ if (source.authority.length > 0) {
1360
+ lines.push("## Authority (ranked)", "");
1361
+ lines.push(...source.authority.map((entry, index) => `${String(index + 1)}. ${entry.path} — ${entry.note}`));
1362
+ lines.push("");
1363
+ }
1364
+ const partitions = [
1365
+ ["Read", source.manifest.read],
1366
+ ["Edit", source.manifest.edit],
1367
+ ["Locked", source.manifest.locked],
1368
+ ["Forbidden", source.manifest.forbidden]
1369
+ ];
1370
+ if (partitions.some((partition) => partition[1].length > 0)) {
1371
+ lines.push("## Manifest", "");
1372
+ for (const [heading, entries] of partitions) {
1373
+ if (entries.length === 0) continue;
1374
+ lines.push(`### ${heading}`, "");
1375
+ lines.push(...entries.map((entry) => `- ${entry.path} — ${entry.note}`));
1376
+ lines.push("");
1377
+ }
1378
+ }
1379
+ if (source.outcomes.length > 0) {
1380
+ lines.push("## Outcomes", "");
1381
+ lines.push(...source.outcomes.map((entry) => `${String(entry.rank)}. ${entry.text}${entry.required ? " (required)" : " (optional)"}`));
1382
+ lines.push("");
1383
+ }
1384
+ const prose = [
1385
+ ["Rules", source.rules],
1386
+ ["Invariants", source.invariants],
1387
+ ["Assumptions", source.assumptions]
1388
+ ];
1389
+ for (const [heading, entries] of prose) {
1390
+ if (entries.length === 0) continue;
1391
+ lines.push(`## ${heading}`, "");
1392
+ lines.push(...entries.map((entry) => `- ${entry}`));
1393
+ lines.push("");
1394
+ }
1395
+ if (source.givens.length > 0) {
1396
+ lines.push("## Givens", "");
1397
+ lines.push(...source.givens.map((entry) => `- ${entry.category} · ${entry.name}: ${entry.value}`));
1398
+ lines.push("");
1399
+ }
1400
+ if (source.examples.length > 0) {
1401
+ lines.push("## Examples", "");
1402
+ for (const entry of source.examples) lines.push(...exampleToLines(entry));
1403
+ lines.push("");
1404
+ }
1405
+ if (source.citations.length > 0) {
1406
+ lines.push("## Citations (trust order)", "");
1407
+ lines.push(...source.citations.map((entry, index) => `${String(index + 1)}. ${entry.name} — ${entry.note} — ${entry.url}`));
1408
+ lines.push("");
1409
+ }
1410
+ if (source.gaps.length > 0) {
1411
+ lines.push("## Gaps", "");
1412
+ lines.push(...source.gaps.map((entry) => {
1413
+ const mark = entry.blocking ? "blocking" : "open";
1414
+ const candidates = entry.candidates === void 0 ? "" : ` (candidates: ${entry.candidates.join(", ")})`;
1415
+ return `- [${mark}] ${entry.field}: ${entry.question}${candidates}`;
1416
+ }));
1417
+ lines.push("");
1418
+ }
1419
+ if (source.risks.length > 0) {
1420
+ lines.push("## Risks", "");
1421
+ lines.push(...source.risks.map((entry) => `- ${entry.severity}: ${entry.text} — ${entry.mitigation}`));
1422
+ lines.push("");
1423
+ }
1424
+ lines.push("## Output", "", `- format: ${source.output.format}`);
1425
+ const refinements = [
1426
+ ["sections", source.output.sections],
1427
+ ["include", source.output.include],
1428
+ ["exclude", source.output.exclude]
1429
+ ];
1430
+ for (const [label, entries] of refinements) {
1431
+ if (entries === void 0 || entries.length === 0) continue;
1432
+ lines.push(`- ${label}: ${entries.join(", ")}`);
1433
+ }
1434
+ lines.push("");
1435
+ if (source.proofs.length > 0) {
1436
+ lines.push("## Proofs", "");
1437
+ lines.push(...source.proofs.map((entry) => `- ${entry.text} — \`${entry.command}\``));
1438
+ lines.push("");
1439
+ }
1440
+ return lines.join("\n");
1441
+ }
1442
+ /**
1443
+ * Project a brief into a `/goal` completion condition.
1444
+ *
1445
+ * @remarks
1446
+ * The proofs' commands VERBATIM plus a turn cap — the goal never adds a condition the
1447
+ * brief does not carry.
1448
+ *
1449
+ * @param source - The brief to render.
1450
+ * @param turns - The turn cap; defaults to `DEFAULT_BRIEF_TURNS`.
1451
+ * @returns The one-line completion condition.
1452
+ *
1453
+ * @example
1454
+ * ```ts
1455
+ * import { brief, briefToGoal, proof, task } from '@orkestrel/brief'
1456
+ *
1457
+ * briefToGoal(brief(task('test', 'code', 'Cover the gate.'), { proofs: [proof('x', 'npm test')] }))
1458
+ * // 'Done when every proof passes: npm test exits 0. Cap: 16 turns.'
1459
+ * ```
1460
+ */
1461
+ function briefToGoal(input, turns = 16) {
1462
+ const source = snapshotBrief(input);
1463
+ return `Done when every proof passes: ${source.proofs.length === 0 ? "no proofs recorded" : source.proofs.map((entry) => `${entry.command} exits 0`).join("; ")}. Cap: ${String(turns)} turns.`;
1464
+ }
1465
+ /**
1466
+ * Project a brief into a subagent `Dispatch`.
1467
+ *
1468
+ * @remarks
1469
+ * `edit` is exactly `manifest.edit`, so two dispatches whose `edit` sets do not intersect
1470
+ * can run concurrently under the same brief without conflict.
1471
+ *
1472
+ * `authority` is exactly `brief.authority` in rank order, and it is a SEPARATE axis from the
1473
+ * four permission sets rather than a fifth partition — a ranked path normally also appears in
1474
+ * `read` or `locked`, because the executor has to open what it obeys. It is projected as
1475
+ * paths so a machine consumer never has to parse `prompt`, which is written for a model.
1476
+ *
1477
+ * @param source - The brief to project.
1478
+ * @returns The dispatch — the rendered prompt, the ranked authority, and the four path sets.
1479
+ *
1480
+ * @example
1481
+ * ```ts
1482
+ * import { brief, briefToDispatch, manifest, reference, task } from '@orkestrel/brief'
1483
+ *
1484
+ * const draft = brief(task('migrate', 'code', 'Migrate the stores.'), {
1485
+ * authority: [reference('AGENTS.md', 'project law')],
1486
+ * manifest: manifest({ edit: [reference('src/core/stores/**', 'the legacy stores')] }),
1487
+ * })
1488
+ * briefToDispatch(draft).edit // ['src/core/stores/**']
1489
+ * briefToDispatch(draft).authority // ['AGENTS.md']
1490
+ * ```
1491
+ */
1492
+ function briefToDispatch(input) {
1493
+ const source = snapshotBrief(input);
1494
+ return {
1495
+ prompt: briefToMarkdown(source),
1496
+ authority: source.authority.map((entry) => entry.path),
1497
+ read: source.manifest.read.map((entry) => entry.path),
1498
+ edit: source.manifest.edit.map((entry) => entry.path),
1499
+ locked: source.manifest.locked.map((entry) => entry.path),
1500
+ forbidden: source.manifest.forbidden.map((entry) => entry.path)
1501
+ };
1502
+ }
1503
+ /**
1504
+ * Derive one imperative statement from free text.
1505
+ *
1506
+ * @remarks
1507
+ * Whitespace collapses, the first character uppercases, and a terminator is appended
1508
+ * when the text carries none. Nothing else is invented.
1509
+ *
1510
+ * @param text - The raw request text.
1511
+ * @returns The statement, or `''` for empty or whitespace-only text.
1512
+ *
1513
+ * @example
1514
+ * ```ts
1515
+ * import { deriveStatement } from '@orkestrel/brief'
1516
+ *
1517
+ * deriveStatement(' clean up useForm ') // 'Clean up useForm.'
1518
+ * deriveStatement('') // ''
1519
+ * ```
1520
+ */
1521
+ function deriveStatement(text) {
1522
+ const collapsed = collapseWhitespace(text);
1523
+ if (collapsed.length === 0) return "";
1524
+ const capitalized = collapsed.charAt(0).toUpperCase() + collapsed.slice(1);
1525
+ return /[.!?]$/u.test(capitalized) ? capitalized : `${capitalized}.`;
1526
+ }
1527
+ /**
1528
+ * Derive a `Task` from an interprets `Intent` through the caller's vocabularies.
1529
+ *
1530
+ * @remarks
1531
+ * The vocabularies are the CALLER's policy: this maps and never guesses. An action or
1532
+ * domain the caller did not map — or mapped to an off-vocabulary value — yields
1533
+ * `undefined` rather than an invented task. Inherited keys never resolve.
1534
+ *
1535
+ * @param intent - The classified intent from an interpret pipeline.
1536
+ * @param text - The text the statement derives from.
1537
+ * @param actions - Maps an intent action onto a closed `TaskOperation`.
1538
+ * @param domains - Maps an intent domain onto a closed `TaskDomain`.
1539
+ * @returns The derived `Task`, or `undefined` when either side is unmapped.
1540
+ *
1541
+ * @example
1542
+ * ```ts
1543
+ * import { deriveTask } from '@orkestrel/brief'
1544
+ *
1545
+ * const intent = { action: 'migrate', domain: 'code', confidence: 1 }
1546
+ * deriveTask(intent, 'migrate the stores', { migrate: 'migrate' }, { code: 'code' })
1547
+ * // { operation: 'migrate', domain: 'code', statement: 'Migrate the stores.' }
1548
+ * deriveTask(intent, 'migrate the stores', {}, { code: 'code' }) // undefined
1549
+ * ```
1550
+ */
1551
+ function deriveTask(intent, text, actions, domains) {
1552
+ const operation = Object.hasOwn(actions, intent.action) ? actions[intent.action] : void 0;
1553
+ const domain = Object.hasOwn(domains, intent.domain) ? domains[intent.domain] : void 0;
1554
+ if (!isTaskOperation(operation) || !isTaskDomain(domain)) return void 0;
1555
+ const statement = deriveStatement(text);
1556
+ return statement.length === 0 ? void 0 : task(operation, domain, statement);
1557
+ }
1558
+ /**
1559
+ * Derive `Given[]` from an interprets `Entity[]`.
1560
+ *
1561
+ * @remarks
1562
+ * Every extracted entity becomes one `extracted` fact. A nameless entity is dropped; an
1563
+ * object value renders through interprets `canonicalize`, so the text is key-order stable.
1564
+ *
1565
+ * @param entities - The entities an interpret pipeline extracted.
1566
+ * @returns One `Given` per named entity, in extraction order.
1567
+ *
1568
+ * @example
1569
+ * ```ts
1570
+ * import { deriveGivens } from '@orkestrel/brief'
1571
+ *
1572
+ * deriveGivens([
1573
+ * { name: 'value', value: 3, provenance: { category: 'extracted' }, confidence: 1 },
1574
+ * ]) // [{ category: 'extracted', name: 'value', value: '3' }]
1575
+ * ```
1576
+ */
1577
+ function deriveGivens(entities) {
1578
+ return entities.filter((entity) => entity.name.length > 0).map((entity) => given("extracted", entity.name, typeof entity.value === "string" ? entity.value : typeof entity.value === "object" && entity.value !== null ? canonicalize(entity.value) : String(entity.value)));
1579
+ }
1580
+ /**
1581
+ * Derive `Gap[]` from an interprets `Ambiguity[]`.
1582
+ *
1583
+ * @remarks
1584
+ * A REQUIRED ambiguity becomes a BLOCKING gap — the gate must fail closed on it. The
1585
+ * rest stay open, to be answered with a recorded assumption. An array field path flattens
1586
+ * through reasons `formatField`.
1587
+ *
1588
+ * @param ambiguities - The ambiguities an interpret pipeline surfaced.
1589
+ * @returns One `Gap` per ambiguity, in surfacing order.
1590
+ *
1591
+ * @example
1592
+ * ```ts
1593
+ * import { deriveGaps } from '@orkestrel/brief'
1594
+ *
1595
+ * deriveGaps([{ field: 'output', question: 'Diff or files?', candidates: [], required: true }])
1596
+ * // [{ field: 'output', question: 'Diff or files?', blocking: true }]
1597
+ * ```
1598
+ */
1599
+ function deriveGaps(ambiguities) {
1600
+ return ambiguities.map((ambiguity) => {
1601
+ const candidates = ambiguity.candidates.filter((candidate) => candidate.length > 0);
1602
+ return gap(formatField(ambiguity.field), ambiguity.question, {
1603
+ blocking: ambiguity.required,
1604
+ ...candidates.length === 0 ? {} : { candidates }
1605
+ });
1606
+ });
1607
+ }
1608
+ //#endregion
1609
+ //#region src/core/parsers.ts
1610
+ /**
1611
+ * Parse a JSON string into a `Brief`.
1612
+ *
1613
+ * @remarks
1614
+ * The parse-then-trust boundary for a stored brief, a tool argument, or an agent's
1615
+ * emission. Invalid JSON, an extra key, an off-vocabulary literal, and a missing section
1616
+ * all fail the same way — `undefined`, never a throw. Coerce a bare vocabulary value with
1617
+ * `parseEnum` from `@orkestrel/contract` against the exported tuple instead.
1618
+ *
1619
+ * @param value - The JSON text to parse.
1620
+ * @returns The `Brief` when the parsed value satisfies `isBrief`, otherwise `undefined`.
1621
+ *
1622
+ * @example
1623
+ * ```ts
1624
+ * import { parseBrief } from '@orkestrel/brief'
1625
+ *
1626
+ * parseBrief('not json') // undefined
1627
+ * parseBrief('{"task":{"operation":"plan","domain":"ops","statement":"x."}}') // undefined
1628
+ * ```
1629
+ */
1630
+ function parseBrief(value) {
1631
+ return parseJSONAs(value, isBrief);
1632
+ }
1633
+ //#endregion
1634
+ //#region src/core/BriefManager.ts
1635
+ /**
1636
+ * The self-owning, versioned and content-hashed brief registry.
1637
+ *
1638
+ * @remarks
1639
+ * Record ids are MINTED from each brief's own content hash unless the caller names one,
1640
+ * so registering unchanged content twice is a version no-op and two callers who compiled
1641
+ * the same request land on the same id with no coordination. A call after `destroy()`
1642
+ * throws `BriefError('DESTROYED', …)`.
1643
+ *
1644
+ * @example
1645
+ * ```ts
1646
+ * import { BriefManager, brief, task } from '@orkestrel/brief'
1647
+ *
1648
+ * const briefs = new BriefManager()
1649
+ * const record = briefs.add(brief(task('document', 'writing', 'Write the brief guide.')))
1650
+ * record.id === record.hash // true
1651
+ * briefs.destroy()
1652
+ * ```
1653
+ */
1654
+ var BriefManager = class {
1655
+ #emitter;
1656
+ #records = /* @__PURE__ */ new Map();
1657
+ #destroyed = false;
1658
+ constructor(options) {
1659
+ const hooks = options?.on;
1660
+ const failed = options?.error;
1661
+ const seeds = options?.briefs ?? [];
1662
+ const staged = /* @__PURE__ */ new Map();
1663
+ for (const entry of seeds) {
1664
+ const record = this.#stage(entry, staged);
1665
+ staged.set(record.id, record);
1666
+ }
1667
+ this.#emitter = new Emitter({
1668
+ ...hooks === void 0 ? {} : { on: hooks },
1669
+ ...failed === void 0 ? {} : { error: failed }
1670
+ });
1671
+ for (const record of staged.values()) this.#commit(record);
1672
+ }
1673
+ get emitter() {
1674
+ return this.#emitter;
1675
+ }
1676
+ get size() {
1677
+ return this.#records.size;
1678
+ }
1679
+ has(id) {
1680
+ this.#refuseDestroyed();
1681
+ return this.#records.has(id);
1682
+ }
1683
+ brief(id) {
1684
+ this.#refuseDestroyed();
1685
+ return this.#records.get(id);
1686
+ }
1687
+ briefs() {
1688
+ this.#refuseDestroyed();
1689
+ return [...this.#records.values()];
1690
+ }
1691
+ add(source, options) {
1692
+ this.#refuseDestroyed();
1693
+ const record = this.#stage(source, this.#records, options);
1694
+ this.#commit(record);
1695
+ return record;
1696
+ }
1697
+ remove(target) {
1698
+ this.#refuseDestroyed();
1699
+ if (target === void 0) {
1700
+ for (const id of [...this.#records.keys()]) this.#discard(id);
1701
+ return;
1702
+ }
1703
+ if (typeof target === "string") return this.#discard(target);
1704
+ let removed = true;
1705
+ for (const id of new Set(target)) if (!this.#discard(id)) removed = false;
1706
+ return removed;
1707
+ }
1708
+ destroy() {
1709
+ if (this.#destroyed) return;
1710
+ this.#destroyed = true;
1711
+ this.#records.clear();
1712
+ this.#emitter.emit("destroy");
1713
+ this.#emitter.destroy();
1714
+ }
1715
+ #stage(source, against, options) {
1716
+ const owned = snapshotBrief(source);
1717
+ const hash = briefToHash(owned);
1718
+ if (owned.hash !== void 0 && owned.hash !== hash) throw new BriefError("INVALID", "Brief carries a hash that does not describe it", {
1719
+ field: "hash",
1720
+ hash: owned.hash
1721
+ });
1722
+ const trace = briefToTrace(owned);
1723
+ if (owned.trace !== void 0 && owned.trace !== trace) throw new BriefError("INVALID", "Brief carries a trace that does not describe it", {
1724
+ field: "trace",
1725
+ trace: owned.trace
1726
+ });
1727
+ const id = options?.id ?? hash;
1728
+ const previous = against.get(id);
1729
+ return Object.freeze({
1730
+ id,
1731
+ brief: owned,
1732
+ version: previous === void 0 ? 1 : this.#version(previous, owned, hash),
1733
+ hash
1734
+ });
1735
+ }
1736
+ #commit(record) {
1737
+ this.#records.set(record.id, record);
1738
+ this.#emitter.emit("add", record.id);
1739
+ }
1740
+ #version(previous, incoming, hash) {
1741
+ if (previous.hash !== hash) return previous.version + 1;
1742
+ if (briefToContent(previous.brief) === briefToContent(incoming)) return previous.version;
1743
+ throw new BriefError("INVALID", "Two different briefs share one content hash — name them with distinct ids", {
1744
+ field: "hash",
1745
+ hash
1746
+ });
1747
+ }
1748
+ #discard(id) {
1749
+ if (!this.#records.delete(id)) return false;
1750
+ this.#emitter.emit("remove", id);
1751
+ return true;
1752
+ }
1753
+ #refuseDestroyed() {
1754
+ if (this.#destroyed) throw new BriefError("DESTROYED", "BriefManager has been destroyed");
1755
+ }
1756
+ };
1757
+ //#endregion
1758
+ //#region src/core/BriefCompiler.ts
1759
+ /**
1760
+ * The compilation orchestrator — the four-stage `[interpret, draft, gate, pin]` pipeline.
1761
+ *
1762
+ * @remarks
1763
+ * `compile` is genuinely SYNCHRONOUS and never throws for a brief it cannot emit: a
1764
+ * blocking gap, a refused gate, and a thrown stage all yield a visible INCOMPLETE
1765
+ * `Briefing`. It owns the engines it created and BORROWS the ones passed in, so
1766
+ * `destroy()` releases only what it made.
1767
+ *
1768
+ * @example
1769
+ * ```ts
1770
+ * import { BriefCompiler, proof, task } from '@orkestrel/brief'
1771
+ *
1772
+ * const compiler = new BriefCompiler()
1773
+ * const briefing = compiler.compile({
1774
+ * task: task('audit', 'code', 'Audit the barrel for undocumented exports.'),
1775
+ * outcomes: [{ rank: 1, text: 'every export appears in the guide', required: true }],
1776
+ * proofs: [proof('parity passes', 'npm run test:guides')],
1777
+ * })
1778
+ * briefing.brief !== undefined // true — the presence of the brief IS the completeness test
1779
+ * compiler.destroy()
1780
+ * ```
1781
+ */
1782
+ var BriefCompiler = class {
1783
+ #emitter;
1784
+ #interpret;
1785
+ #reason;
1786
+ #ownInterpret;
1787
+ #ownReason;
1788
+ #actions;
1789
+ #domains;
1790
+ #destroyed = false;
1791
+ constructor(options) {
1792
+ const hooks = options?.on;
1793
+ const failed = options?.error;
1794
+ const borrowedInterpret = options?.interpret;
1795
+ const borrowedReason = options?.reason;
1796
+ this.#emitter = new Emitter({
1797
+ ...hooks === void 0 ? {} : { on: hooks },
1798
+ ...failed === void 0 ? {} : { error: failed }
1799
+ });
1800
+ this.#ownInterpret = borrowedInterpret === void 0;
1801
+ this.#ownReason = borrowedReason === void 0;
1802
+ this.#interpret = borrowedInterpret ?? createInterpret();
1803
+ this.#reason = borrowedReason ?? createReason({ reasoners: [createLogicalReasoner()] });
1804
+ this.#actions = options?.actions ?? {};
1805
+ this.#domains = options?.domains ?? {};
1806
+ }
1807
+ get emitter() {
1808
+ return this.#emitter;
1809
+ }
1810
+ get interpret() {
1811
+ return this.#interpret;
1812
+ }
1813
+ get reason() {
1814
+ return this.#reason;
1815
+ }
1816
+ compile(input) {
1817
+ this.#refuseDestroyed();
1818
+ const stages = [];
1819
+ const failures = [];
1820
+ const taken = attempt(() => this.#snapshot(input));
1821
+ if (!taken.success) {
1822
+ const message = errorToMessage(taken.error);
1823
+ stages.push(Object.freeze({
1824
+ stage: "draft",
1825
+ input: {},
1826
+ error: message
1827
+ }));
1828
+ failures.push(Object.freeze({
1829
+ stage: "draft",
1830
+ code: "DRAFT_FAILED",
1831
+ message
1832
+ }));
1833
+ this.#emitter.emit("error", taken.error);
1834
+ return this.#refuse(void 0, void 0, [], void 0, stages, failures);
1835
+ }
1836
+ const owned = taken.value;
1837
+ const interpretation = this.#read(owned, stages, failures);
1838
+ const drafted = attempt(() => this.#draft(owned, interpretation, this.#unresolved(interpretation, failures)));
1839
+ if (!drafted.success) {
1840
+ const message = errorToMessage(drafted.error);
1841
+ stages.push(Object.freeze({
1842
+ stage: "draft",
1843
+ input: owned,
1844
+ error: message
1845
+ }));
1846
+ failures.push(Object.freeze({
1847
+ stage: "draft",
1848
+ code: "DRAFT_FAILED",
1849
+ message
1850
+ }));
1851
+ this.#emitter.emit("error", drafted.error);
1852
+ return this.#refuse(interpretation, void 0, [], void 0, stages, failures);
1853
+ }
1854
+ const draft = drafted.value;
1855
+ stages.push(Object.freeze({
1856
+ stage: "draft",
1857
+ input: owned,
1858
+ output: draft
1859
+ }));
1860
+ const questions = findBlockingGaps(draft);
1861
+ const subject = Object.freeze(briefToSubject(draft));
1862
+ const ruled = attempt(() => this.gate(draft));
1863
+ if (ruled.success) stages.push(Object.freeze({
1864
+ stage: "gate",
1865
+ input: subject,
1866
+ output: ruled.value
1867
+ }));
1868
+ else {
1869
+ const message = errorToMessage(ruled.error);
1870
+ stages.push(Object.freeze({
1871
+ stage: "gate",
1872
+ input: subject,
1873
+ error: message
1874
+ }));
1875
+ failures.push(Object.freeze({
1876
+ stage: "gate",
1877
+ code: "GATE_FAILED",
1878
+ message
1879
+ }));
1880
+ this.#emitter.emit("error", ruled.error);
1881
+ }
1882
+ const verdict = ruled.success ? ruled.value : void 0;
1883
+ const unready = findUnmetRules(draft);
1884
+ if (unready.length > 0 || verdict === void 0 || !verdict.conclusion) {
1885
+ const refusal = this.#blockage(questions, unready, verdict);
1886
+ if (refusal !== void 0) failures.push(Object.freeze(refusal));
1887
+ return this.#refuse(interpretation, draft, questions, verdict, stages, failures);
1888
+ }
1889
+ const stamped = attempt(() => pinBrief(draft));
1890
+ if (!stamped.success) {
1891
+ const message = errorToMessage(stamped.error);
1892
+ stages.push(Object.freeze({
1893
+ stage: "pin",
1894
+ input: draft,
1895
+ error: message
1896
+ }));
1897
+ failures.push(Object.freeze({
1898
+ stage: "pin",
1899
+ code: "PIN_FAILED",
1900
+ message
1901
+ }));
1902
+ this.#emitter.emit("error", stamped.error);
1903
+ return this.#refuse(interpretation, draft, questions, verdict, stages, failures);
1904
+ }
1905
+ const pinned = stamped.value;
1906
+ stages.push(Object.freeze({
1907
+ stage: "pin",
1908
+ input: draft,
1909
+ output: pinned
1910
+ }));
1911
+ const briefing = Object.freeze({
1912
+ ...interpretation === void 0 ? {} : { interpretation },
1913
+ brief: pinned,
1914
+ questions: Object.freeze([]),
1915
+ verdict,
1916
+ stages: Object.freeze([...stages]),
1917
+ failures: Object.freeze([...failures]),
1918
+ digest: digestValue({
1919
+ brief: pinned,
1920
+ questions: [],
1921
+ failures
1922
+ })
1923
+ });
1924
+ this.#emitter.emit("compile", briefing);
1925
+ return briefing;
1926
+ }
1927
+ gate(source) {
1928
+ this.#refuseDestroyed();
1929
+ const ruled = attempt(() => this.#own(this.#reason.reason(briefToSubject(source), gateDefinition())));
1930
+ if (!ruled.success) throw new BriefError("GATE_FAILED", errorToMessage(ruled.error), {
1931
+ stage: "gate",
1932
+ field: "reason"
1933
+ });
1934
+ const verdict = ruled.value;
1935
+ if (!isLogicalVerdict(verdict)) throw new BriefError("GATE_FAILED", "The gate reasoner returned a non-logical result", {
1936
+ stage: "gate",
1937
+ field: "reasoning"
1938
+ });
1939
+ return verdict;
1940
+ }
1941
+ destroy() {
1942
+ if (this.#destroyed) return;
1943
+ this.#destroyed = true;
1944
+ if (this.#ownInterpret) this.#interpret.destroy();
1945
+ if (this.#ownReason) this.#reason.destroy();
1946
+ this.#emitter.emit("destroy");
1947
+ this.#emitter.destroy();
1948
+ }
1949
+ #snapshot(input) {
1950
+ return freezeDeep(structuredClone(input));
1951
+ }
1952
+ #own(value) {
1953
+ const cloned = attempt(() => structuredClone(value));
1954
+ return freezeDeep(cloned.success ? cloned.value : value);
1955
+ }
1956
+ #read(input, stages, failures) {
1957
+ const text = input.text;
1958
+ if (text === void 0) return input.interpretation;
1959
+ const read = attempt(() => this.#own(this.#interpret.interpret(text)));
1960
+ if (read.success) {
1961
+ stages.push(Object.freeze({
1962
+ stage: "interpret",
1963
+ input: text,
1964
+ output: read.value
1965
+ }));
1966
+ return read.value;
1967
+ }
1968
+ const message = errorToMessage(read.error);
1969
+ stages.push(Object.freeze({
1970
+ stage: "interpret",
1971
+ input: text,
1972
+ error: message
1973
+ }));
1974
+ failures.push(Object.freeze({
1975
+ stage: "interpret",
1976
+ code: "INTERPRET_FAILED",
1977
+ message
1978
+ }));
1979
+ this.#emitter.emit("error", read.error);
1980
+ return input.interpretation;
1981
+ }
1982
+ #blockage(questions, unready, verdict) {
1983
+ if (questions.length > 0) return {
1984
+ stage: "gate",
1985
+ code: "BLOCKED",
1986
+ message: `${String(questions.length)} blocking gap(s)`
1987
+ };
1988
+ if (unready.length > 0) return {
1989
+ stage: "gate",
1990
+ code: "BLOCKED",
1991
+ message: `Gate refused: ${unready.join(", ")}`
1992
+ };
1993
+ if (!isLogicalVerdict(verdict)) return void 0;
1994
+ const refused = verdict.rules.filter((entry) => !entry.conclusion).map((entry) => entry.id).join(", ");
1995
+ if (refused.length === 0) return {
1996
+ stage: "gate",
1997
+ code: "BLOCKED",
1998
+ message: "Gate refused: the supplied reasoner named no failing rule"
1999
+ };
2000
+ return {
2001
+ stage: "gate",
2002
+ code: "BLOCKED",
2003
+ message: `Gate refused: ${refused}`
2004
+ };
2005
+ }
2006
+ #unresolved(interpretation, failures) {
2007
+ if (interpretation !== void 0) return [];
2008
+ if (!failures.some((entry) => entry.stage === "interpret")) return [];
2009
+ return [gap("gaps", "The interpret stage failed, so the request is unread and its unknowns are unknown", { blocking: true })];
2010
+ }
2011
+ #draft(input, interpretation, unresolved) {
2012
+ const derived = interpretation === void 0 ? void 0 : deriveTask(interpretation.intent, interpretation.text, this.#actions, this.#domains);
2013
+ const subject = input.task ?? derived;
2014
+ if (subject === void 0) throw new BriefError("DRAFT_FAILED", "No task: supply BriefInput.task, or map the intent through the actions and domains vocabularies", {
2015
+ stage: "draft",
2016
+ field: "task"
2017
+ });
2018
+ return snapshotBrief(brief(subject, {
2019
+ authority: input.authority ?? [],
2020
+ manifest: input.manifest ?? manifest(),
2021
+ outcomes: input.outcomes ?? [],
2022
+ rules: input.rules ?? [],
2023
+ invariants: input.invariants ?? [],
2024
+ givens: [...interpretation === void 0 ? [] : deriveGivens(interpretation.entities), ...input.givens ?? []],
2025
+ examples: input.examples ?? [],
2026
+ assumptions: input.assumptions ?? [],
2027
+ citations: input.citations ?? [],
2028
+ gaps: [
2029
+ ...interpretation === void 0 ? [] : deriveGaps(interpretation.ambiguities),
2030
+ ...unresolved,
2031
+ ...input.gaps ?? []
2032
+ ],
2033
+ risks: input.risks ?? [],
2034
+ output: input.output ?? output("markdown"),
2035
+ proofs: input.proofs ?? []
2036
+ }));
2037
+ }
2038
+ #refuse(interpretation, draft, questions, verdict, stages, failures) {
2039
+ const asked = Object.freeze([...questions]);
2040
+ const briefing = Object.freeze({
2041
+ ...interpretation === void 0 ? {} : { interpretation },
2042
+ questions: asked,
2043
+ ...verdict === void 0 ? {} : { verdict },
2044
+ stages: Object.freeze([...stages]),
2045
+ failures: Object.freeze([...failures]),
2046
+ digest: digestValue({
2047
+ ...draft === void 0 ? {} : { brief: draft },
2048
+ questions,
2049
+ failures
2050
+ })
2051
+ });
2052
+ this.#emitter.emit("block", asked);
2053
+ return briefing;
2054
+ }
2055
+ #refuseDestroyed() {
2056
+ if (this.#destroyed) throw new BriefError("DESTROYED", "BriefCompiler has been destroyed");
2057
+ }
2058
+ };
2059
+ //#endregion
2060
+ //#region src/core/factories.ts
2061
+ /**
2062
+ * Create a compilation orchestrator.
2063
+ *
2064
+ * @remarks
2065
+ * With no engines supplied the compiler wires its own: a default `createInterpret()`
2066
+ * (empty vocabularies, so `options.actions` / `options.domains` drive `deriveTask`) and a
2067
+ * `createReason` carrying one `LogicalReasoner` for the gate. Pass your own to share
2068
+ * instances or observe their emitters — the compiler destroys ONLY what it created.
2069
+ *
2070
+ * @param options - Engines to borrow, the two intent vocabularies, and emitter hooks.
2071
+ * @returns A working {@link BriefCompilerInterface}.
2072
+ *
2073
+ * @example
2074
+ * ```ts
2075
+ * import { createBriefCompiler } from '@orkestrel/brief'
2076
+ *
2077
+ * const compiler = createBriefCompiler({ actions: { refactor: 'refactor' }, domains: { code: 'code' } })
2078
+ * compiler.destroy()
2079
+ * ```
2080
+ */
2081
+ function createBriefCompiler(options) {
2082
+ return new BriefCompiler(options);
2083
+ }
2084
+ /**
2085
+ * Create a brief registry.
2086
+ *
2087
+ * @param options - An optional seed collection plus emitter hooks.
2088
+ * @returns A working {@link BriefManagerInterface}.
2089
+ *
2090
+ * @example
2091
+ * ```ts
2092
+ * import { createBriefManager } from '@orkestrel/brief'
2093
+ *
2094
+ * const briefs = createBriefManager()
2095
+ * briefs.size // 0
2096
+ * briefs.destroy()
2097
+ * ```
2098
+ */
2099
+ function createBriefManager(options) {
2100
+ return new BriefManager(options);
2101
+ }
2102
+ /**
2103
+ * Compile `briefShape` into a guard, parser, JSON Schema, and seeded generator bundle.
2104
+ *
2105
+ * @remarks
2106
+ * The schema is what a tool boundary needs — hand it to `schemaToParameters` — and
2107
+ * `generate(seededRandom(n))` yields a reproducible on-contract brief for tests. This
2108
+ * bundle and the hand-composed `isBrief` are two independent mechanisms over one
2109
+ * vocabulary; `tests/src/core/shapers.test.ts` is what holds them in lockstep.
2110
+ *
2111
+ * @returns A `ContractInterface` over `Brief`.
2112
+ *
2113
+ * @example
2114
+ * ```ts
2115
+ * import { createBriefContract } from '@orkestrel/brief'
2116
+ * import { schemaToParameters, seededRandom } from '@orkestrel/contract'
2117
+ *
2118
+ * const contract = createBriefContract()
2119
+ * schemaToParameters(contract.schema) // the open tool-parameters record, no `as` anywhere
2120
+ * contract.generate(seededRandom(42)) // a reproducible on-contract brief
2121
+ * ```
2122
+ */
2123
+ function createBriefContract() {
2124
+ return createContract(briefShape);
2125
+ }
2126
+ //#endregion
2127
+ export { BLANK_PATTERN, BriefCompiler, BriefError, BriefManager, DEFAULT_BRIEF_TURNS, GATE_ID, LINE_BREAK_PATTERN, OUTPUT_FORMATS, RISK_SEVERITIES, SINGLE_LINE_PATTERN, TASK_DOMAINS, TASK_OPERATIONS, assertBrief, brief, briefShape, briefToContent, briefToDispatch, briefToGoal, briefToHash, briefToMarkdown, briefToSubject, briefToTrace, citation, citationShape, countSentences, createBriefCompiler, createBriefContract, createBriefManager, deriveGaps, deriveGivens, deriveStatement, deriveTask, errorToMessage, example, exampleShape, exampleToLines, findBlockingGaps, findManifestOverlaps, findUngrantedAuthority, findUnmetRules, findUnpairedGaps, freezeBranch, freezeDeep, gap, gapShape, gateDefinition, given, givenShape, isBrief, isBriefError, isCitation, isExample, isGap, isGiven, isLine, isLogicalVerdict, isManifest, isObject, isOutcome, isOutput, isOutputFormat, isProof, isReference, isRisk, isRiskSeverity, isRuleVerdict, isTask, isTaskDomain, isTaskOperation, isText, lineShape, manifest, manifestShape, outcome, outcomeShape, output, outputShape, parseBrief, pinBrief, proof, proofShape, reference, referenceShape, risk, riskShape, snapshotBrief, task, taskShape, textShape, validateBrief };
2128
+
2129
+ //# sourceMappingURL=index.js.map