bareloop 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +704 -0
- package/README.md +13 -4
- package/bareloop.context.md +351 -32
- package/package.json +1 -1
- package/src/authorflow.js +1280 -0
- package/src/authoring.js +1296 -0
- package/src/authorjob.js +793 -0
- package/src/authorscout.js +706 -0
- package/src/bridges.js +2 -2
- package/src/declaredclose.js +519 -0
- package/src/exits.js +8 -2
- package/src/index.js +72 -0
- package/src/job.js +91 -10
- package/src/kinds.js +1354 -0
- package/src/ledger.js +70 -9
- package/src/plan.js +57 -101
- package/src/planrun.js +554 -75
- package/src/ralph.js +27 -5
- package/src/reuse.js +135 -11
- package/src/root.js +8 -1
- package/src/run.js +29 -3
- package/src/text.js +120 -0
- package/src/validate.js +121 -0
- package/src/workbranch.js +261 -0
- package/types/authorflow.d.ts +720 -0
- package/types/authoring.d.ts +447 -0
- package/types/authorjob.d.ts +241 -0
- package/types/authorscout.d.ts +307 -0
- package/types/declaredclose.d.ts +174 -0
- package/types/exits.d.ts +14 -0
- package/types/index.d.ts +9 -1
- package/types/job.d.ts +14 -2
- package/types/kinds.d.ts +343 -0
- package/types/plan.d.ts +41 -38
- package/types/planrun.d.ts +125 -18
- package/types/ralph.d.ts +17 -0
- package/types/reuse.d.ts +5 -1
- package/types/run.d.ts +23 -2
- package/types/text.d.ts +84 -0
- package/types/validate.d.ts +50 -0
- package/types/workbranch.d.ts +102 -0
package/src/bridges.js
CHANGED
|
@@ -36,7 +36,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, s
|
|
|
36
36
|
import { join } from 'node:path';
|
|
37
37
|
import { isObj, isNonEmptyString } from './validate.js';
|
|
38
38
|
import { TOOL_MENU, LOCKED_TOOLS } from './job.js';
|
|
39
|
-
import {
|
|
39
|
+
import { closeStagesOf } from './plan.js';
|
|
40
40
|
|
|
41
41
|
export const BRIDGE_SCHEMA = 'bridge-v1';
|
|
42
42
|
|
|
@@ -460,7 +460,7 @@ export function loadGate(bridge, job) {
|
|
|
460
460
|
}
|
|
461
461
|
|
|
462
462
|
// 2. close-stage kinds (v1: names in order)
|
|
463
|
-
const stages = (
|
|
463
|
+
const stages = (closeStagesOf(j) ?? []).map((/** @type {any} */ s) => (isObj(s) ? s.name : null));
|
|
464
464
|
const stored = Array.isArray(b.closeStageNames) ? b.closeStageNames : null;
|
|
465
465
|
if (!stored || stored.length !== stages.length || !stored.every((/** @type {unknown} */ n, /** @type {number} */ i) => n === stages[i])) {
|
|
466
466
|
red('closeStageNames', `bridge close [${(stored ?? ['<none>']).join(' → ')}] vs this job's close [${(stages.length ? stages : ['<none>']).join(' → ')}] — same names in the same order is v1's definition of the same kinds`);
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
// THE RUNTIME BRIDGE — close-authoring v1, module M4a (design record
|
|
2
|
+
// 2026-08-07 D8/D9/D12/D13; gate-2 POC closed 2026-08-08).
|
|
3
|
+
//
|
|
4
|
+
// M1 runs a DECLARATION. The arbiter's shell (`src/ralph.js`) runs an ARGV. This
|
|
5
|
+
// module is the one place those two shapes meet, and it exists because of the
|
|
6
|
+
// integration decision M4 had to make in the open:
|
|
7
|
+
//
|
|
8
|
+
// A declared close is carried by a NEW spec field (`closeDecl`) and executed by
|
|
9
|
+
// M1's own kind executor. It is never COMPILED DOWN to command stages.
|
|
10
|
+
//
|
|
11
|
+
// The rejected alternative is the one that looks cheaper: render each declared
|
|
12
|
+
// stage as a shell one-liner and let the existing `close[]` path run it. That
|
|
13
|
+
// would turn owned kinds back into authored shell strings — the exact thing D3
|
|
14
|
+
// makes inexpressible ("the danger is in the ACTIONS, not the SYNTAX"), and the
|
|
15
|
+
// exact thing the whole rung exists to remove. A declaration whose meaning is a
|
|
16
|
+
// generated `sh -c` is a script with extra steps, and the arbiter would be
|
|
17
|
+
// judging text nobody validated. So: two executors, one CONTRACT, and the
|
|
18
|
+
// contract is what this file holds still.
|
|
19
|
+
//
|
|
20
|
+
// WHAT "one contract" MEANS, concretely — every one of these is a behaviour the
|
|
21
|
+
// command path already had and the declared path must not lose:
|
|
22
|
+
//
|
|
23
|
+
// * FIRST RED WINS, and the deciding stage NAMES itself. A stage after the
|
|
24
|
+
// first red never runs, so a later green cannot mask an earlier failure.
|
|
25
|
+
// * THE FORBIDDEN ZONE (`CLOSE_FAULTS`). A stage that rendered no judgment is
|
|
26
|
+
// NOT a verdict: it routes by its own typed fault (`STOP_FAULTS`, stamped in
|
|
27
|
+
// M1 at the site that observed it) into the arbiter's four rows, so a
|
|
28
|
+
// timeout still offers "raise the close timeout" and a spawn failure still
|
|
29
|
+
// offers "fix the close". Pooling them would erase exactly the decision
|
|
30
|
+
// information the escalation carries.
|
|
31
|
+
// * THE GAP HEADER. `close stage "<name>" failed:` — the SAME spelling, from
|
|
32
|
+
// `stageGap` in ralph.js, because `src/trend.js` parses it to bucket a grade
|
|
33
|
+
// per stage. A second spelling here is the two-transforms class (F9) on the
|
|
34
|
+
// instrument that decides whether a human tops a run up.
|
|
35
|
+
// * THE GAP OUTPUT CONTRACT: every line prefixed, trims announced (M1 owns
|
|
36
|
+
// that), and the whole thing SCRUBBED at this boundary through the one
|
|
37
|
+
// `SECRET_PATTERNS` inventory — M1 deliberately does not scrub, the caller
|
|
38
|
+
// does, and this is the caller.
|
|
39
|
+
// * THE CHECK MENU derives from stage NAMES exactly as before (`checkMenu` in
|
|
40
|
+
// src/job.js reads `name`/`offer`/`needs` and nothing else), so
|
|
41
|
+
// `check-passes(name)` reaches a declared stage with no change at all.
|
|
42
|
+
//
|
|
43
|
+
// TWO THINGS THIS BRIDGE ADDS, both because the declared executor knows more
|
|
44
|
+
// than an exit code does:
|
|
45
|
+
//
|
|
46
|
+
// 1. `gapKeep`. Every declared gap line carries a fixed PREFIX, so the anchored
|
|
47
|
+
// pattern derived from it designates the stage's own failures — which is
|
|
48
|
+
// what Layer R's red-set reads off `stagedClose.find(...).gapKeep`. It is
|
|
49
|
+
// ours, derived mechanically from one constant, and never model-authored:
|
|
50
|
+
// the declaration has no field for it.
|
|
51
|
+
// 2. A TREND VALUE. `count-not-worse` reports a real number, so the grade does
|
|
52
|
+
// not have to be scraped back out of prose. It is donated ONLY for
|
|
53
|
+
// `lower-is-better`, and the omission is the load-bearing half: trend.js
|
|
54
|
+
// reads "improved" as `value < best`, so donating a higher-is-better floor
|
|
55
|
+
// would read a DROPPING test count as convergence and recommend a top-up on
|
|
56
|
+
// a dying run. That is the dangerous direction; a null is the honest one
|
|
57
|
+
// (F6), and the stage position still travels.
|
|
58
|
+
//
|
|
59
|
+
// THE SEED (D8). HEAD at run start, READ and recorded, never typed and never
|
|
60
|
+
// stored in the signed spec — D12: the close stores the counting rule, never the
|
|
61
|
+
// number, so a baseline frozen at signing would judge run 5 against run 1's tree.
|
|
62
|
+
//
|
|
63
|
+
// SEED WORKTREES are scoped to ONE `runDeclaredStages` call unless a caller
|
|
64
|
+
// hands in a shared holder. Cost, measured against safety and stated rather than
|
|
65
|
+
// assumed: at most one checkout per close invocation (M1 shares it across that
|
|
66
|
+
// close's stages), and none at all while the tree is verifiably at seed —
|
|
67
|
+
// precheck and preflight therefore pay nothing. A run-scoped holder would pay
|
|
68
|
+
// once instead of once-per-invocation, but `runPlan` has a dozen return paths and
|
|
69
|
+
// a leaked worktree outlives the process that made it.
|
|
70
|
+
|
|
71
|
+
import { runStage, makeSeedTrees, STOP_FAULTS, EXIT_RED } from './kinds.js';
|
|
72
|
+
import {
|
|
73
|
+
validateDeclaration, classGuards, genreOwnedEnvNames, ungroundedGenreEnv, GENRE_LANGUAGES, TYPES_GENRE,
|
|
74
|
+
VERDICT_CLASSES, LIVE_CLASSES,
|
|
75
|
+
} from './authoring.js';
|
|
76
|
+
import { stageGap } from './ralph.js';
|
|
77
|
+
import { isObj, isNonEmptyString } from './validate.js';
|
|
78
|
+
|
|
79
|
+
/** @typedef {{code: string, path: string, detail: string, [k: string]: any}} Red */
|
|
80
|
+
|
|
81
|
+
/** The genres a `closeDecl` may name. v1 admits exactly one (D13) — a genre
|
|
82
|
+
* classifier is designed the day there is a second genre to classify. */
|
|
83
|
+
export const DECLARED_GENRES = Object.freeze([TYPES_GENRE.name]);
|
|
84
|
+
/**
|
|
85
|
+
* The exact field set of a `closeDecl` — anything else is a smuggle channel, the
|
|
86
|
+
* same rule `STAGE_FIELDS` enforces on a command stage.
|
|
87
|
+
*
|
|
88
|
+
* `genreEnv` is the ARBITER's own record of what it injected (M3's
|
|
89
|
+
* `applyGenreEnv`), and it is here because the injection has to survive being
|
|
90
|
+
* re-read. The flow applies the genre's variable to every env-capable stage
|
|
91
|
+
* AFTER the model's form passed; from then on every re-validation — signing
|
|
92
|
+
* (`prepareSigning`), the job validator (`src/job.js`), the runner
|
|
93
|
+
* (`src/planrun.js`) — sees that key sitting in `params.env` and cannot, from
|
|
94
|
+
* the stage alone, tell our injection from a model-authored guess. Recording it
|
|
95
|
+
* once, in the SIGNED envelope, is what makes the comparison an EQUALITY rather
|
|
96
|
+
* than a presence flag (the same move `checkGuards` makes for guard params), and
|
|
97
|
+
* it is why the model never gets to author it: `authorCloseForJob` composes this
|
|
98
|
+
* field from the flow's own applied map, never from model output. Its keys are
|
|
99
|
+
* constrained to the names the genre OWNS, so it can never become an environment
|
|
100
|
+
* channel of its own.
|
|
101
|
+
*/
|
|
102
|
+
export const CLOSE_DECL_FIELDS = Object.freeze(['genre', 'lang', 'stages', 'notes', 'genreEnv']);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The prefix M1 puts on EVERY gap line of a declared stage (its `ctx.gapKeep`).
|
|
106
|
+
* A literal, so the anchored pattern below is exact.
|
|
107
|
+
*/
|
|
108
|
+
export const DECLARED_GAP_PREFIX = 'close: ';
|
|
109
|
+
/** The `gapKeep` PATTERN for a declared stage, DERIVED from the prefix so the
|
|
110
|
+
* two can never drift. Layer R's red-set and `boundGap`'s keep-block both read
|
|
111
|
+
* it; because every declared line carries the prefix, it designates the stage's
|
|
112
|
+
* whole judged output rather than a hand-picked slice of it. */
|
|
113
|
+
export const DECLARED_GAP_KEEP = `^${DECLARED_GAP_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`;
|
|
114
|
+
|
|
115
|
+
/** The verdict class a DECLARED close may claim. Hard, and by CONSTRUCTION
|
|
116
|
+
* rather than by assertion: every live kind is a mechanical command whose
|
|
117
|
+
* measurement is the truth, and the two judgment kinds are locked out at the
|
|
118
|
+
* catalogue (M2) and absent from the executor (M1). Named as data so the job
|
|
119
|
+
* validator reads one hierarchy table rather than hard-coding a class. */
|
|
120
|
+
export const DECLARED_CLOSE_CLASSES = Object.freeze(['hard']);
|
|
121
|
+
|
|
122
|
+
/** @param {any} job @returns {boolean} */
|
|
123
|
+
export function isDeclaredClose(job) {
|
|
124
|
+
return isObj(job) && isObj(job.closeDecl) && Array.isArray(job.closeDecl.stages);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The declaration's stages as the RUNNER's stage descriptors: the declared
|
|
129
|
+
* `{name, kind, params}` plus the arbiter's own `gapKeep`.
|
|
130
|
+
*
|
|
131
|
+
* `offer` and `needs` pass through untouched when a declaration carries them
|
|
132
|
+
* (v1's schema does not let one), so `checkMenu`'s derivation is unchanged.
|
|
133
|
+
* @param {any} closeDecl
|
|
134
|
+
* @returns {any[]|null} null when this is not a declaration
|
|
135
|
+
*/
|
|
136
|
+
export function declaredStages(closeDecl) {
|
|
137
|
+
if (!isObj(closeDecl) || !Array.isArray(closeDecl.stages)) return null;
|
|
138
|
+
return closeDecl.stages.map((s) => (isObj(s) ? { ...s, gapKeep: DECLARED_GAP_KEEP } : s));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── the spec-level gate ──────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Validate a `closeDecl` as a JOB SPEC field: the envelope (genre, language,
|
|
145
|
+
* notes) plus M2's full declaration validator underneath it.
|
|
146
|
+
*
|
|
147
|
+
* `listing` is what splits the two callers, and the split is deliberate:
|
|
148
|
+
* - the JOB VALIDATOR has no repository, so it passes nothing and gets
|
|
149
|
+
* `grounded: false` — every tree-independent rule runs, the path rule and
|
|
150
|
+
* the scoped-job derivation do not;
|
|
151
|
+
* - the RUNNER has the real seed, so it passes the real listing and gets
|
|
152
|
+
* `grounded: true`. It runs this BEFORE any stage and before any token, so
|
|
153
|
+
* the deferred half is proved rather than skipped.
|
|
154
|
+
* A caller that passes neither a listing nor `deferListing` gets M2's
|
|
155
|
+
* `listing-absent` red, unchanged: a validator handed nothing to check against
|
|
156
|
+
* must refuse, never report a declaration it never examined.
|
|
157
|
+
*
|
|
158
|
+
* THREE checks now ride on the listing, not two: the path rule, the scoped-job
|
|
159
|
+
* derivation, and the GROUNDING of a recorded `genreEnv` (a by-value comparison
|
|
160
|
+
* between two copies inside one hand-editable spec proves they agree, never that
|
|
161
|
+
* the arbiter wrote them — see `ungroundedGenreEnv`). All three are deferred at
|
|
162
|
+
* the job-spec gate and all three are re-run GROUNDED by the runner before any
|
|
163
|
+
* stage executes, so the deferral moves the check rather than dropping it. The
|
|
164
|
+
* grounded gates are `prepareSigning`'s gate 1b — where the signature is minted,
|
|
165
|
+
* so a forged env can never be signed in the first place — and `runPlan`'s
|
|
166
|
+
* pre-flight, which catches a spec edited after signing.
|
|
167
|
+
*
|
|
168
|
+
* `verdictType` is the class the USER picked, and it is REQUIRED here because
|
|
169
|
+
* the guard battery keys off it (PRD v1.57 §2): with no class in hand there is
|
|
170
|
+
* no battery, and M2 would be handed nothing to check D5 against. It arrives
|
|
171
|
+
* from the spec's sibling `verdictType` field on every caller — the job
|
|
172
|
+
* validator, the signing gate and the runner all hold the whole spec.
|
|
173
|
+
*
|
|
174
|
+
* @param {any} closeDecl
|
|
175
|
+
* @param {{at?: string, listing?: string[]|null, deferListing?: boolean,
|
|
176
|
+
* catalogue?: Record<string, any>, verdictType?: string|null}} [opts]
|
|
177
|
+
* @returns {{ok: boolean, reds: Red[], closeDecl: any, grounded: boolean,
|
|
178
|
+
* scoped: {scoped: boolean, via: string|null},
|
|
179
|
+
* ceiling: {class: string, kind: string, stage: string|null, locked: boolean}|null}}
|
|
180
|
+
*/
|
|
181
|
+
export function validateCloseDecl(closeDecl, opts = {}) {
|
|
182
|
+
const { at = 'closeDecl', listing = null, catalogue, verdictType = null } = opts;
|
|
183
|
+
const deferListing = opts.deferListing === true;
|
|
184
|
+
/** @type {Red[]} */
|
|
185
|
+
const reds = [];
|
|
186
|
+
/** @type {(code: string, path: string, detail: string, extra?: object) => void} */
|
|
187
|
+
const red = (code, path, detail, extra = {}) => { reds.push({ code, path, detail, ...extra }); };
|
|
188
|
+
const bail = () => ({ ok: false, reds, closeDecl: null, grounded: false, scoped: { scoped: false, via: null }, ceiling: null });
|
|
189
|
+
|
|
190
|
+
// THE PICKED CLASS, before anything else, because the battery hangs off it.
|
|
191
|
+
// The path is the SPEC's own `verdictType` rather than a `closeDecl` child:
|
|
192
|
+
// the class is the user's separate answer and this red must point at the field
|
|
193
|
+
// they actually set, not at the artefact it invalidates.
|
|
194
|
+
//
|
|
195
|
+
// A locked class reds here as well as on job.js's counted `request-red`, and
|
|
196
|
+
// that is two axes rather than one problem twice: one COUNTS the demand for
|
|
197
|
+
// the class, this one says the close cannot be validated because the class it
|
|
198
|
+
// was picked under has no battery. It is the same split `langOk` already
|
|
199
|
+
// makes, and both bail for the same reason — a validator configured out of its
|
|
200
|
+
// own rule reports a declaration it never examined.
|
|
201
|
+
const picked = String(verdictType);
|
|
202
|
+
if (!VERDICT_CLASSES.includes(picked)) {
|
|
203
|
+
red('class-absent', 'verdictType', `the verdict class the user picked — one of ${VERDICT_CLASSES.join(' | ')}. `
|
|
204
|
+
+ 'It selects this close\'s mandatory guard battery, which has no default and is never inferred');
|
|
205
|
+
return bail();
|
|
206
|
+
}
|
|
207
|
+
if (!LIVE_CLASSES.includes(picked)) {
|
|
208
|
+
red('class-battery-locked', 'verdictType', `"${picked}" is declared-but-locked: v1 builds a guard battery for `
|
|
209
|
+
+ `${LIVE_CLASSES.join(', ')} only, and a close cannot be validated against a battery that does not exist — `
|
|
210
|
+
+ 'validating it against another class\'s guards would sign a close nobody checked');
|
|
211
|
+
return bail();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!isObj(closeDecl)) {
|
|
215
|
+
red('invalid-value', at, 'an authored close DECLARATION: { genre, lang, stages: [...] }');
|
|
216
|
+
return bail();
|
|
217
|
+
}
|
|
218
|
+
for (const key of Object.keys(closeDecl)) {
|
|
219
|
+
if (!CLOSE_DECL_FIELDS.includes(key)) {
|
|
220
|
+
red('unknown-field', `${at}.${key}`, `not a closeDecl field (script bodies, seed refs and frozen thresholds all `
|
|
221
|
+
+ `land here); fields: ${CLOSE_DECL_FIELDS.join(', ')}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (!DECLARED_GENRES.includes(closeDecl.genre)) {
|
|
225
|
+
red('invalid-value', `${at}.genre`, `menu: ${DECLARED_GENRES.join('|')} — v1 admits exactly one genre (D13), and a `
|
|
226
|
+
+ 'genre with no guard battery must refuse rather than author a close without the guards it cannot supply');
|
|
227
|
+
}
|
|
228
|
+
const langOk = GENRE_LANGUAGES.includes(closeDecl.lang);
|
|
229
|
+
if (!langOk) {
|
|
230
|
+
red('invalid-value', `${at}.lang`, `menu: ${GENRE_LANGUAGES.join('|')} — the language selects the genre's guard `
|
|
231
|
+
+ 'battery and its environment, both of which are ours and neither of which has a default');
|
|
232
|
+
}
|
|
233
|
+
// `notes` is NOT checked here. It is M2's rule now (`validateDeclaration`),
|
|
234
|
+
// for the reason the divergence caused: this gate runs at SIGNING, and a rule
|
|
235
|
+
// that lives only at signing cannot be fed back as a revision — the run pays
|
|
236
|
+
// for a close, the authoring loop accepts it, and the job dies on a defect the
|
|
237
|
+
// model was never shown. One spelling, at the gate that can still revise, and
|
|
238
|
+
// the red still arrives here with this gate's own `at` prefix. It rides the
|
|
239
|
+
// `langOk` bail below like every other declaration-shape red: with no language
|
|
240
|
+
// there is no declaration to validate at all.
|
|
241
|
+
//
|
|
242
|
+
// Without a language there is no guard battery and no owned-env list, so M2
|
|
243
|
+
// would be handed nothing to check D5 against — and a validator configured out
|
|
244
|
+
// of its own rule reports a declaration it never examined. Refuse here instead.
|
|
245
|
+
if (!langOk) return bail();
|
|
246
|
+
|
|
247
|
+
// The recorded injection (see CLOSE_DECL_FIELDS). Constrained to the names the
|
|
248
|
+
// genre owns and to non-empty string values: an unowned name here would be an
|
|
249
|
+
// environment channel wearing the arbiter's coat, and an empty value is not the
|
|
250
|
+
// same instruction as an unset variable (`genreEnv` never ships one).
|
|
251
|
+
const owned = genreOwnedEnvNames(closeDecl.lang);
|
|
252
|
+
/** @type {Record<string, string>} */
|
|
253
|
+
const envInjected = {};
|
|
254
|
+
if (closeDecl.genreEnv !== undefined) {
|
|
255
|
+
if (!isObj(closeDecl.genreEnv)) {
|
|
256
|
+
red('invalid-value', `${at}.genreEnv`, 'the genre environment this close was authored under, as an object of '
|
|
257
|
+
+ 'non-empty string values — it records what the ARBITER injected, and nothing else belongs in it');
|
|
258
|
+
} else {
|
|
259
|
+
for (const [name, value] of Object.entries(closeDecl.genreEnv)) {
|
|
260
|
+
if (!owned.includes(name)) {
|
|
261
|
+
red('invalid-value', `${at}.genreEnv.${name}`, `the ${closeDecl.lang} genre does not own ${name} — this field `
|
|
262
|
+
+ `records the genre's own injection only (${owned.length ? owned.join(', ') : 'this genre owns none'}), `
|
|
263
|
+
+ 'never an environment of its own');
|
|
264
|
+
} else if (!isNonEmptyString(value)) {
|
|
265
|
+
red('invalid-value', `${at}.genreEnv`, `${name} is recorded with no value — an empty variable is not the same `
|
|
266
|
+
+ 'instruction as an unset one, and the genre never builds one');
|
|
267
|
+
} else envInjected[name] = value;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// GROUNDED, wherever there is a tree to ground against. The by-VALUE check
|
|
272
|
+
// above proves the envelope and the stage AGREE — and both copies sit in the
|
|
273
|
+
// same hand-editable spec, so agreement is free to a forger: `genreEnv` and
|
|
274
|
+
// `params.env` both reading `/tmp/attacker-stubs` is perfectly self-consistent
|
|
275
|
+
// and grades a checkout nobody signed. The seed tree is the thing the forger
|
|
276
|
+
// does not own, so every element of a recorded value must SELECT from the
|
|
277
|
+
// listing — the same trick hamr's path rule already uses, and an exact fit for
|
|
278
|
+
// what `applyGenreEnv` can produce (see `ungroundedGenreEnv`).
|
|
279
|
+
//
|
|
280
|
+
// It runs at both gates that hold a listing (`prepareSigning`'s grounded gate
|
|
281
|
+
// 1b and the runner's pre-flight re-validation) and cannot run at the deferred
|
|
282
|
+
// job-spec gate, which has no repository — that gate's deferral is already
|
|
283
|
+
// stated above, and the runner re-validates GROUNDED before any stage runs, so
|
|
284
|
+
// this is moved rather than skipped.
|
|
285
|
+
if (Array.isArray(listing) && listing.length > 0) {
|
|
286
|
+
for (const u of ungroundedGenreEnv(closeDecl.lang, envInjected, listing)) {
|
|
287
|
+
red('genre-env-ungrounded', `${at}.genreEnv.${u.name}`, `${u.name} is recorded as the genre's own injection but `
|
|
288
|
+
+ `"${u.element}" matches nothing in the seed tree — the arbiter builds this value by joining source prefixes it `
|
|
289
|
+
+ 'already filtered against the listing, so a path the tree does not contain is not one it can have written',
|
|
290
|
+
{ name: u.name, element: u.element, resolved: u.resolved });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const v = validateDeclaration(closeDecl, {
|
|
295
|
+
...(catalogue ? { catalogue } : {}),
|
|
296
|
+
// BOTH are forwarded, always. Forwarding only one would hide the case M2
|
|
297
|
+
// reds as `listing-conflict` — a caller that defers the tree-grounded half
|
|
298
|
+
// AND hands over a tree does not know which gate it is running, and silently
|
|
299
|
+
// honouring one of the two is how a signature ends up covering a check
|
|
300
|
+
// nobody performed.
|
|
301
|
+
listing,
|
|
302
|
+
...(deferListing ? { deferListing: true } : {}),
|
|
303
|
+
guards: classGuards({ verdictType: picked, lang: closeDecl.lang }),
|
|
304
|
+
verdictType: picked,
|
|
305
|
+
envOwned: owned,
|
|
306
|
+
// POST-injection: an owned name is accepted only when its value is EXACTLY
|
|
307
|
+
// the recorded one. `{}` (nothing recorded) keeps the pre-injection rule, so
|
|
308
|
+
// a declaration that authored the variable itself is still refused.
|
|
309
|
+
envInjected,
|
|
310
|
+
});
|
|
311
|
+
for (const r of v.reds) reds.push({ ...r, path: r.path ? `${at}.${r.path}` : at });
|
|
312
|
+
|
|
313
|
+
const ok = reds.length === 0;
|
|
314
|
+
return {
|
|
315
|
+
ok,
|
|
316
|
+
reds,
|
|
317
|
+
closeDecl: ok ? { ...closeDecl, stages: v.declaration.stages } : null,
|
|
318
|
+
grounded: v.grounded,
|
|
319
|
+
scoped: v.scoped,
|
|
320
|
+
ceiling: v.ceiling,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── the execution bridge ─────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* The genre's mandatory guard NAMES for a declaration — which stages are D5's
|
|
328
|
+
* guards and which are the job's own work. The seed-verdict read (D9.3) needs
|
|
329
|
+
* exactly this split: `changed-from-seed` is RED at its own seed by
|
|
330
|
+
* construction, so counting it as work would let a close with nothing to do
|
|
331
|
+
* pass the "something must be red at seed" gate.
|
|
332
|
+
* The battery keys off the CLASS the user picked (PRD v1.57 §2), so the class
|
|
333
|
+
* comes in beside the declaration. A class or a language with no battery hands
|
|
334
|
+
* back `[]` — this reads a split, it does not enforce one, and every caller
|
|
335
|
+
* reaches it only after `validateCloseDecl` has already refused both cases.
|
|
336
|
+
* @param {any} closeDecl @param {string} verdictType @returns {string[]}
|
|
337
|
+
*/
|
|
338
|
+
export function guardNames(closeDecl, verdictType) {
|
|
339
|
+
if (!isObj(closeDecl) || !GENRE_LANGUAGES.includes(closeDecl.lang)) return [];
|
|
340
|
+
if (!LIVE_CLASSES.includes(String(verdictType))) return [];
|
|
341
|
+
return classGuards({ verdictType: String(verdictType), lang: closeDecl.lang }).map((g) => g.name);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* One declared stage's result, in the ARBITER's verdict vocabulary.
|
|
346
|
+
*
|
|
347
|
+
* `notes` is the DIAGNOSTIC channel, and it exists because M1's kinds announce
|
|
348
|
+
* some things whichever way a stage lands: a declared env var that was dropped,
|
|
349
|
+
* the scope filter's own arithmetic. A red carries those out inside its gap; a
|
|
350
|
+
* green and an instrument stop had nowhere to put them and this function used to
|
|
351
|
+
* compute the text and discard it. A green measured over a filtered population
|
|
352
|
+
* is still a filtered number, and the operator reads why — checking the GREENS
|
|
353
|
+
* is doctrine, and an audit trail that only survives a red cannot do it.
|
|
354
|
+
*
|
|
355
|
+
* It is `notes` and NOT `.gap`, deliberately. Every consumer downstream guards
|
|
356
|
+
* with `if (gap)`, so a green carrying one would read as revision-worthy and a
|
|
357
|
+
* stop carrying one would look like a verdict it never rendered. Nothing routes
|
|
358
|
+
* on this field, nothing bounds on it, and no verdict moves because of it; it
|
|
359
|
+
* rides the spine records that already spread the whole verdict
|
|
360
|
+
* (`close-precheck`, `outer-close`, ralph's `close-verdict`).
|
|
361
|
+
* @param {any} r an M1 `StageResult`
|
|
362
|
+
* @param {(s: string) => string} redact
|
|
363
|
+
* @returns {{verdict: string, notes?: string, gap?: string, detail?: string,
|
|
364
|
+
* exitCode?: number, judgedCount?: number}} `notes` is absent, never empty —
|
|
365
|
+
* a field that is always there says nothing; a RED carries its diagnostics
|
|
366
|
+
* inside `gap` and so has none of its own.
|
|
367
|
+
*/
|
|
368
|
+
function translate(r, redact) {
|
|
369
|
+
const gapText = redact((r.gapLines ?? []).join('\n'));
|
|
370
|
+
/** absent rather than empty: a field that is always there says nothing */
|
|
371
|
+
const notes = gapText ? { notes: gapText } : {};
|
|
372
|
+
if (r.verdict === 'green') return { verdict: 'satisfied', judgedCount: 1, ...notes };
|
|
373
|
+
if (r.verdict === 'red') {
|
|
374
|
+
return {
|
|
375
|
+
verdict: 'needs_revision',
|
|
376
|
+
// Never falsy: every consumer guards with `if (gap)`, and an empty red
|
|
377
|
+
// would silently kill gap feedback, the after-red hooks and the stall
|
|
378
|
+
// detector at once (the same rule `runClose` states for its own path).
|
|
379
|
+
gap: gapText || `(stage "${r.stage}" rendered a red with no output)`,
|
|
380
|
+
exitCode: r.exitCode ?? EXIT_RED,
|
|
381
|
+
judgedCount: 1,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
// instrument-stop — NOT a verdict. The fault is a typed field stamped where the
|
|
385
|
+
// fault was observed, never sniffed out of the stop's prose. `detail` is the
|
|
386
|
+
// stop's own sentence; `notes` is the whole rendered output around it, which is
|
|
387
|
+
// where a measurement's already-computed arithmetic lives (`stopped`'s own
|
|
388
|
+
// `notes` argument) — the difference between hunting a parser bug and reading
|
|
389
|
+
// the scope.
|
|
390
|
+
return {
|
|
391
|
+
verdict: r.detail?.fault ?? STOP_FAULTS.FAILED,
|
|
392
|
+
detail: redact(String(r.detail?.stop ?? `stage "${r.stage}" rendered no judgment`)),
|
|
393
|
+
...notes,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Run a DECLARED close (or a check's chain out of one) and return `runClose`'s
|
|
399
|
+
* verdict shape, so every consumer downstream — the precheck, ralph's judge, the
|
|
400
|
+
* `CLOSE_FAULTS` routing, the trend reader, Layer R — is unchanged.
|
|
401
|
+
*
|
|
402
|
+
* The extras a declared close can honestly add ride alongside rather than
|
|
403
|
+
* replacing anything: `declared: true` marks the shape, `value`/`baseline` carry
|
|
404
|
+
* the deciding stage's own measurement, and `trendValue` is the ONE number the
|
|
405
|
+
* trend reader may compare (see the header for why a higher-is-better stage
|
|
406
|
+
* donates none).
|
|
407
|
+
*
|
|
408
|
+
* @param {any[]} stages declared stage descriptors, already validated
|
|
409
|
+
* @param {(s: string) => string} [redact] the scrub, applied at THIS boundary
|
|
410
|
+
* @param {{timeoutMs?: number, cwd?: string, seedRef?: string,
|
|
411
|
+
* seedTrees?: any, gapCap?: number, maxBuffer?: number, baselineMode?: 'auto'|'worktree'}} [opts]
|
|
412
|
+
* @returns {Promise<any>}
|
|
413
|
+
*/
|
|
414
|
+
export async function runDeclaredStages(stages, redact = (s) => s, opts = {}) {
|
|
415
|
+
const { timeoutMs, cwd, seedRef, seedTrees: shared, gapCap, maxBuffer, baselineMode } = opts;
|
|
416
|
+
if (!isNonEmptyString(cwd) || !isNonEmptyString(seedRef)) {
|
|
417
|
+
// Refuse rather than guess. A declared close measures against a SEED; a
|
|
418
|
+
// missing one is the close unable to run, not a red about the worker.
|
|
419
|
+
return {
|
|
420
|
+
verdict: STOP_FAULTS.FAILED,
|
|
421
|
+
detail: 'a declared close needs both the repository it judges (cwd) and the seed it measures against (seedRef) — '
|
|
422
|
+
+ 'neither has a default, because a defaulted baseline grades a tree nobody chose',
|
|
423
|
+
stage: null,
|
|
424
|
+
stages: [],
|
|
425
|
+
declared: true,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
const owned = !shared;
|
|
429
|
+
const seedTrees = shared ?? makeSeedTrees();
|
|
430
|
+
/** @type {any} */
|
|
431
|
+
const ctx = {
|
|
432
|
+
workdir: cwd,
|
|
433
|
+
seedRef,
|
|
434
|
+
gapKeep: DECLARED_GAP_PREFIX,
|
|
435
|
+
...(timeoutMs !== undefined ? { timeoutMsDefault: timeoutMs } : {}),
|
|
436
|
+
...(gapCap !== undefined ? { gapCap } : {}),
|
|
437
|
+
...(maxBuffer !== undefined ? { maxBuffer } : {}),
|
|
438
|
+
...(baselineMode !== undefined ? { baselineMode } : {}),
|
|
439
|
+
seedTrees,
|
|
440
|
+
};
|
|
441
|
+
try {
|
|
442
|
+
/** @type {any[]} */
|
|
443
|
+
const ran = [];
|
|
444
|
+
/** @type {any} */
|
|
445
|
+
let last = { verdict: 'satisfied', judgedCount: 1 };
|
|
446
|
+
for (const st of stages) {
|
|
447
|
+
const r = await runStage(st, ctx);
|
|
448
|
+
const t = translate(r, redact);
|
|
449
|
+
ran.push({
|
|
450
|
+
name: st?.name ?? null,
|
|
451
|
+
verdict: t.verdict,
|
|
452
|
+
...(t.exitCode !== undefined ? { exitCode: t.exitCode } : {}),
|
|
453
|
+
...(r.value !== null && r.value !== undefined ? { value: r.value } : {}),
|
|
454
|
+
...(r.baseline !== null && r.baseline !== undefined ? { baseline: r.baseline } : {}),
|
|
455
|
+
// PER STAGE, because the summary below carries only the LAST translation:
|
|
456
|
+
// an earlier green's announcement would otherwise be lost behind the
|
|
457
|
+
// stage that decided.
|
|
458
|
+
...(t.notes !== undefined ? { notes: t.notes } : {}),
|
|
459
|
+
});
|
|
460
|
+
last = t;
|
|
461
|
+
if (t.verdict !== 'satisfied') {
|
|
462
|
+
return {
|
|
463
|
+
...t,
|
|
464
|
+
stage: st?.name ?? null,
|
|
465
|
+
stages: ran,
|
|
466
|
+
declared: true,
|
|
467
|
+
value: r.value ?? null,
|
|
468
|
+
baseline: r.baseline ?? null,
|
|
469
|
+
trendValue: trendValueOf(st, r),
|
|
470
|
+
// the same header the command path writes, from the SAME function —
|
|
471
|
+
// `src/trend.js` parses it to bucket the grade per stage
|
|
472
|
+
...(t.gap ? { gap: stageGap(/** @type {string} */ (st?.name), t.gap) } : {}),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return { ...last, stages: ran, declared: true, stage: undefined };
|
|
477
|
+
} finally {
|
|
478
|
+
if (owned) await seedTrees.cleanup();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* The ONE number the trend reader may compare, and the omissions are the point.
|
|
484
|
+
* `trend.js` reads improvement as `value < best`, so:
|
|
485
|
+
* - `lower-is-better` counts donate their value (a fault count falling IS
|
|
486
|
+
* convergence);
|
|
487
|
+
* - `higher-is-better` floors donate NOTHING. A test-count floor dropping is
|
|
488
|
+
* the run getting WORSE, and donating it would read as convergence and
|
|
489
|
+
* recommend a top-up on a dying run — the one direction trend.js names as
|
|
490
|
+
* dangerous. F6's answer applies: unknown, never a number that means the
|
|
491
|
+
* opposite of what it says.
|
|
492
|
+
* - a stage with no measurement at all donates nothing, for the same reason.
|
|
493
|
+
* The stage POSITION still travels either way, so "the run got further than it
|
|
494
|
+
* ever had" is readable on every declared close.
|
|
495
|
+
* @param {any} stage @param {any} r the M1 StageResult
|
|
496
|
+
* @returns {number|null}
|
|
497
|
+
*/
|
|
498
|
+
function trendValueOf(stage, r) {
|
|
499
|
+
if (typeof r?.value !== 'number' || !Number.isFinite(r.value)) return null;
|
|
500
|
+
return stage?.params?.direction === 'lower-is-better' ? r.value : null;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* The trend reader's input for ONE close verdict, whichever executor produced
|
|
505
|
+
* it. A command close hands over its gap and `readGrade` scrapes stage and count
|
|
506
|
+
* out of the text; a declared close already KNOWS both, so it says them instead
|
|
507
|
+
* of round-tripping them through prose it would then have to parse back.
|
|
508
|
+
*
|
|
509
|
+
* Byte-identical to the previous call for a command close: `{ gap }`, with
|
|
510
|
+
* `stage`/`value` left UNDEFINED so `trend.record` falls through to `readGrade`
|
|
511
|
+
* exactly as before. Passing `stage: null` instead would silently disable that
|
|
512
|
+
* fallback — undefined and null are different answers here.
|
|
513
|
+
* @param {any} v a `runClose`-shaped verdict
|
|
514
|
+
* @returns {{gap?: string, stage?: string|null, value?: number|null}}
|
|
515
|
+
*/
|
|
516
|
+
export function closeGrade(v) {
|
|
517
|
+
if (v?.declared !== true) return { gap: v?.gap };
|
|
518
|
+
return { gap: v.gap, stage: v.stage ?? null, value: v.trendValue ?? null };
|
|
519
|
+
}
|
package/src/exits.js
CHANGED
|
@@ -33,8 +33,14 @@ import { globToPrefix } from './validate.js';
|
|
|
33
33
|
* `not ok`/`FAILED` names — the mechanical gap the F46 conversion mechanism
|
|
34
34
|
* feeds the worker (F28 reintroduced). This cap is a defensive backstop above
|
|
35
35
|
* boundGap's own envelope: a normal bounded gap passes through intact, and only
|
|
36
|
-
* a pathological seam that returned an unbounded gap would ever be trimmed.
|
|
37
|
-
|
|
36
|
+
* a pathological seam that returned an unbounded gap would ever be trimmed.
|
|
37
|
+
*
|
|
38
|
+
* Exported for the ONE reason `boundGap`/`GAP_KEEP_TRIM_MARKER` are (src/ralph.js):
|
|
39
|
+
* the replan brief re-bounds this same artifact one seam later (`closeGapBlock`), and
|
|
40
|
+
* a second hand-spelled ceiling there would be a second answer to "how big may a
|
|
41
|
+
* check gap be" — the two would drift on exactly the property that matters, whether
|
|
42
|
+
* the `not ok`/`FAILED` names survive. */
|
|
43
|
+
export const CHECK_GAP_MAX = 12000;
|
|
38
44
|
|
|
39
45
|
/** @typedef {{ type: string, pass: boolean, detail?: string, fault?: string }} ExitResult
|
|
40
46
|
* `fault` present means the INSTRUMENT could not judge (unwired snapshot/seam,
|
package/src/index.js
CHANGED
|
@@ -62,3 +62,75 @@ export { renderListing, selectionPrompt } from './selection.js';
|
|
|
62
62
|
// `runReuse`'s own entry.
|
|
63
63
|
export { validateEnvelope, resolveTrySpec, resolveReuse, reuseSpecHash, selectBridge, runReuse, REUSE_GRADED_RED, readResume, resumeTreeGate } from './reuse.js';
|
|
64
64
|
export { classifyIncidents, foldLedger, ledgerDeltas, updateLedger, LEDGER_CLASSES } from './ledger.js';
|
|
65
|
+
// THE WORK BRANCH (PRD v1.57 §3). `workBranchName` is exported so an operator runner can
|
|
66
|
+
// SHOW, before the approval gate, which branch the run will work on — the same reason the
|
|
67
|
+
// resolved per-try hash is printed there: a bound the human is agreeing to should not be
|
|
68
|
+
// discovered from the spine afterwards. `prepareWorkBranch` rides with it because a
|
|
69
|
+
// runner that resumes reads the branch off the dead spine and may want to verify it is
|
|
70
|
+
// still there before spending anything. The runner applies the rule itself either way —
|
|
71
|
+
// neither export is a way to opt out of it.
|
|
72
|
+
export { workBranchName, prepareWorkBranch, WORK_BRANCH_PREFIX, WORK_BRANCH_RE } from './workbranch.js';
|
|
73
|
+
// ── CLOSE AUTHORING v1 (gate 4) — the user declares what done means ──────────
|
|
74
|
+
// The public surface is settled ONCE, here, at M4 (M2's header flagged the
|
|
75
|
+
// naming collision and deferred it rather than exporting piecemeal).
|
|
76
|
+
//
|
|
77
|
+
// THE COLLISION, resolved: the kind executor's grading entry is exported as
|
|
78
|
+
// `runDeclaredClose`, and ralph's shipped `runClose(argv, redact, opts)` keeps
|
|
79
|
+
// its name. Two reasons, in order. (1) `runClose` is a DOCUMENTED adopter
|
|
80
|
+
// contract — it is in bareloop.context.md and in the exports map — and renaming
|
|
81
|
+
// a shipped function to make room for a new one is a breaking change bought for
|
|
82
|
+
// nothing. (2) The new name says what actually differs: one runs a DECLARATION,
|
|
83
|
+
// the other an argv. `runDeclaredClose` / `runDeclaredStages` / `runStages` read
|
|
84
|
+
// as a family, which is what they are.
|
|
85
|
+
//
|
|
86
|
+
// M1 — the kind executor. A runner needs `runStage`/`seedRead` to build its own
|
|
87
|
+
// gate (that is what `prepareSigning` does with them), and `seedAtHead` because
|
|
88
|
+
// D8's seed is READ at run start rather than typed into a spec.
|
|
89
|
+
export {
|
|
90
|
+
runDeclaredClose, runStage, seedRead, seedAtHead, seedListing, changedSet, makeSeedTrees,
|
|
91
|
+
LIVE_KINDS, STOP_FAULTS, JUDGED_MARKER, EXIT_GREEN, EXIT_RED, EXIT_STOP,
|
|
92
|
+
} from './kinds.js';
|
|
93
|
+
// M2 — what a declaration may SAY, and whether one said it legally. The
|
|
94
|
+
// catalogue and the genre are DATA an integrating UI renders (the kind menu, the
|
|
95
|
+
// guard batteries it must show the user under D5 and cannot let them remove).
|
|
96
|
+
export {
|
|
97
|
+
validateDeclaration, normalizeDeclaration, KIND_CATALOGUE, CATALOGUE_KINDS, CATALOGUE_LIVE_KINDS,
|
|
98
|
+
LOCKED_KINDS, TYPES_GENRE, TYPES_GENRE_TEMPLATE, GENRE_LANGUAGES, genreEnv, genreOwnedEnvNames,
|
|
99
|
+
DIRECTIONS, BASELINES, MAX_STAGES,
|
|
100
|
+
// the verdict-class surface (PRD v1.57 §1–§2): the radio's own menu, the guard
|
|
101
|
+
// battery keyed off it, and the ceiling that makes the pick a promise
|
|
102
|
+
VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES, CLASS_BATTERIES, classGuards, closeCeiling,
|
|
103
|
+
} from './authoring.js';
|
|
104
|
+
// M3 — the looking half and the writing half. `authorClose` is the grounded
|
|
105
|
+
// loop; `runAuthorScout`/`buildSeedListing` are exported because a caller that
|
|
106
|
+
// already paid for a survey passes it back in rather than buying a second one.
|
|
107
|
+
export {
|
|
108
|
+
runAuthorScout, buildSeedListing, seedFileList, classifySurvey, AUTHOR_SCOUT_VERBS,
|
|
109
|
+
// the malformed-class retry ladder: the hardcoded ceiling, the typed causes it
|
|
110
|
+
// reads, and the closed set of causes that may fire it (2026-08-09, PRD v1.58)
|
|
111
|
+
SCOUT_ATTEMPTS, SURVEY_CAUSES, SCOUT_RETRY_CAUSES,
|
|
112
|
+
} from './authorscout.js';
|
|
113
|
+
/** the ONE scrubbed-persist boundary for a model's raw output, and its announced
|
|
114
|
+
* bound — exported because every future writer of a raw must reach for this one
|
|
115
|
+
* rather than spell a second scrub (PRD v1.58) */
|
|
116
|
+
export { scrubRaw, RAW_PERSIST_MAX, RAW_TRIM_MARKER } from './text.js';
|
|
117
|
+
export {
|
|
118
|
+
authorClose, authorPrompt, declarationSchema, makeLoopGenerate, MAX_REVISIONS,
|
|
119
|
+
QUESTION_SETS, GREEN_QUESTIONS, CLASS_STATEMENTS, questionsFor, requiredAnswersFor,
|
|
120
|
+
} from './authorflow.js';
|
|
121
|
+
// M4a — the runtime bridge. `validateCloseDecl` is the spec-level gate (the job
|
|
122
|
+
// validator's own branch calls it); `runDeclaredStages` is the executor seam a
|
|
123
|
+
// runner other than `runPlan` would need; `closeStagesOf` is the ONE staging
|
|
124
|
+
// every close consumer reads, widened to both fields.
|
|
125
|
+
export {
|
|
126
|
+
validateCloseDecl, runDeclaredStages, declaredStages, isDeclaredClose, guardNames, closeGrade,
|
|
127
|
+
DECLARED_GENRES, DECLARED_GAP_PREFIX, DECLARED_GAP_KEEP, DECLARED_CLOSE_CLASSES, CLOSE_DECL_FIELDS,
|
|
128
|
+
} from './declaredclose.js';
|
|
129
|
+
export { closeStagesOf } from './plan.js';
|
|
130
|
+
// M4b — the interview, the refusal, the composition, and D9's three gates.
|
|
131
|
+
// `prepareSigning` returns the resolved spec's HASH and the seed evidence; it
|
|
132
|
+
// never signs — the approvals array and the human's word are unchanged.
|
|
133
|
+
export {
|
|
134
|
+
runInterview, authorCloseForJob, assembleSpec, prepareSigning, refusalEvents, refuseLockedKind,
|
|
135
|
+
GENRE, REFUSAL_LIB, REFUSAL_CATEGORY, AUTHORED_SPEC_FIELDS,
|
|
136
|
+
} from './authorjob.js';
|