bareloop 0.41.0 → 0.42.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/README.md +13 -21
  3. package/bareloop.context.md +44 -4
  4. package/package.json +4 -3
  5. package/src/authorflow.js +337 -29
  6. package/src/authoring.js +92 -7
  7. package/src/authorjob.js +78 -16
  8. package/src/authorreadout.js +89 -5
  9. package/src/authorrun.js +14 -5
  10. package/src/authorscout.js +108 -0
  11. package/src/declaredclose.js +23 -12
  12. package/src/facts.js +317 -0
  13. package/src/factwords.js +129 -0
  14. package/src/inputlook.js +301 -0
  15. package/src/job.js +2 -1
  16. package/src/jobblock.js +7 -2
  17. package/src/kinds.js +84 -3
  18. package/src/labelscan.js +170 -0
  19. package/src/pagefetch.js +125 -0
  20. package/src/panel/authorroutes.js +15 -2
  21. package/src/panel/authorsession.js +99 -22
  22. package/src/panel/index.html +33 -7
  23. package/src/panel/jobplan.js +34 -11
  24. package/src/panel/server.js +19 -3
  25. package/src/planrun.js +3 -2
  26. package/src/rubricstep.js +804 -0
  27. package/src/source.js +166 -1
  28. package/src/u-readout.js +1 -1
  29. package/src/userrun.js +14 -2
  30. package/types/authorflow.d.ts +267 -13
  31. package/types/authoring.d.ts +29 -1
  32. package/types/authorjob.d.ts +7 -5
  33. package/types/authorreadout.d.ts +27 -2
  34. package/types/authorscout.d.ts +70 -0
  35. package/types/declaredclose.d.ts +4 -2
  36. package/types/facts.d.ts +71 -0
  37. package/types/factwords.d.ts +42 -0
  38. package/types/inputlook.d.ts +238 -0
  39. package/types/jobblock.d.ts +8 -1
  40. package/types/labelscan.d.ts +38 -0
  41. package/types/pagefetch.d.ts +71 -0
  42. package/types/panel/authorsession.d.ts +18 -2
  43. package/types/panel/jobplan.d.ts +12 -4
  44. package/types/panel/server.d.ts +4 -2
  45. package/types/rubricstep.d.ts +286 -0
  46. package/types/source.d.ts +94 -0
  47. package/types/userrun.d.ts +11 -0
package/src/source.js CHANGED
@@ -23,14 +23,16 @@
23
23
  // on rather than catch.
24
24
 
25
25
  import { createHash } from 'node:crypto';
26
+ import { tmpdir } from 'node:os';
26
27
  import { existsSync, lstatSync, readFileSync } from 'node:fs';
27
- import { mkdir, readdir, readFile, writeFile, lstat, stat, chmod, access, copyFile, cp, open, rm, realpath, readlink, symlink, constants as fsConstants } from 'node:fs/promises';
28
+ import { mkdir, mkdtemp, readdir, readFile, writeFile, lstat, stat, chmod, access, copyFile, cp, open, rm, realpath, readlink, symlink, constants as fsConstants } from 'node:fs/promises';
28
29
  import { dirname, join, relative, resolve, sep, basename, isAbsolute } from 'node:path';
29
30
  import { git } from './kinds.js';
30
31
  import { addWorktree, hideBareloopDir } from './worktree.js';
31
32
  import { MAX_BUFFER } from './kinds.js';
32
33
  import { PROVIDER_TIMEOUT_MS } from './clock.js';
33
34
  import { scanSecrets, SECRET_PATTERNS, SECRET_PATTERN_NAMES } from './validate.js';
35
+ import { fetchPageText, isWebUrl } from './pagefetch.js';
34
36
 
35
37
  /** @typedef {{stop: string, code: string}} SourceRefusal a named, non-throwing stop */
36
38
 
@@ -1223,3 +1225,166 @@ export async function proveInputFiles(inputs) {
1223
1225
  }
1224
1226
  return { ok: true, repo, root };
1225
1227
  }
1228
+
1229
+ /** the per-file ceiling for a rubric input: bareguard's `cited` check searches a quote in a source only up to this size
1230
+ * (its `quoteSourceMaxBytes`); a bigger file could never be cited from, so the door refuses it at $0 */
1231
+ export const RUBRIC_INPUT_MAX_BYTES = 5 * 1024 * 1024;
1232
+
1233
+ /**
1234
+ * Does this source manifest belong to a rubric job on local files (`proveRubricInputs` wrote it)? One spelling for both
1235
+ * doors (the panel session and the CLI author run): a non-repo source that froze numbered, labelled input files.
1236
+ * @param {Record<string, any>|null|undefined} manifest
1237
+ * @returns {boolean}
1238
+ */
1239
+ export function isRubricFilesManifest(manifest) {
1240
+ return !!manifest && manifest.kind !== 'repo' && Array.isArray(manifest.inputs) && manifest.inputs.length > 0;
1241
+ }
1242
+
1243
+ /**
1244
+ * PART 1a (docs/product/ITEM33-BUILD.md, "Addendum 2026-10-12"): the front door for a RUBRIC job whose inputs are the
1245
+ * person's own LOCAL FILES, not a repo. Each numbered `label: /absolute/file` line is proven at $0 and frozen into the
1246
+ * same hidden-git tree a plain folder gets (`prepareSource`, which also refuses a secret shape, an environment file
1247
+ * and a symlink), under `input/`; the manifest then also records `inputs: [{label, rel, sha256}]` — the bytes the
1248
+ * rubric's `cited` check will search a quote in, hashed once, here. A line that is a web address is fetched ONCE with
1249
+ * barebrowse (PART 1c, read only) and its text frozen under `input/` like a file (the manifest row also records its `url`,
1250
+ * so run start can fetch it again); a `search:` line is a named refusal (a later build); so is a relative path, a missing file, a folder, a file over
1251
+ * {@link RUBRIC_INPUT_MAX_BYTES}, and text that is not valid UTF-8. Every refusal is plain words naming the line and
1252
+ * ends "Nothing spent." — nothing here touches a model.
1253
+ * @param {{inputs: {n: number, label: string, value: string}[], into: string, destination?: string, fetchUrl?: (url: string) => Promise<{ok: true, text: string, url: string}|{ok: false, error: string}>}} o `fetchUrl` - how a web address is read (default barebrowse via `fetchPageText`; a test injects a stub, never the network)
1254
+ * @returns {Promise<{ok: true, prep: {stop: null, into: string, tree: string, manifestPath: string, manifest: Record<string, any>}}|{ok: false, error: string, code: string}>}
1255
+ */
1256
+ export async function proveRubricInputs({ inputs, into, destination, fetchUrl = (u) => fetchPageText(u) }) {
1257
+ /** @param {string} code @param {number} n @param {string} msg @returns {{ok: false, error: string, code: string}} */
1258
+ const bad = (code, n, msg) => ({ ok: false, code, error: `line ${n}: ${msg} Nothing spent.` });
1259
+ if (!Array.isArray(inputs) || inputs.length === 0) return { ok: false, code: 'inputs-missing', error: 'Inputs is required. Nothing spent.' };
1260
+ /** @type {Set<string>} */
1261
+ const labels = new Set();
1262
+ /** @type {{label: string, name: string, buf: Buffer, url?: string}[]} */
1263
+ const staged = [];
1264
+ /** @type {Set<string>} */
1265
+ const names = new Set();
1266
+ const utf8 = new TextDecoder('utf-8', { fatal: true });
1267
+ for (const inp of inputs) {
1268
+ const { n, label, value } = inp;
1269
+ if (/^search$/i.test(label) || (/^[a-z][a-z0-9+.-]*:\/\//i.test(value) && !isWebUrl(value))) {
1270
+ return bad('web-input', n, 'a search, or an address that is not http or https, is not an input yet.');
1271
+ }
1272
+ if (labels.has(label.toLowerCase())) return bad('label-repeat', n, `the label "${label}" is already used by an earlier line.`);
1273
+ labels.add(label.toLowerCase());
1274
+ if (isWebUrl(value)) {
1275
+ const got = await fetchUrl(value.trim());
1276
+ if (!got.ok) return bad('input-url-unreadable', n, `${got.error.replace(/[.\s]+$/, '')}.`);
1277
+ const pageBuf = Buffer.from(got.text, 'utf8');
1278
+ const leak = secretPatternNames(got.text);
1279
+ if (leak.length) return bad('input-url-secret', n, `the page at ${got.url} carries a known secret shape (${leak.join(', ')}) - refused, secrets never enter a job.`);
1280
+ const stem = label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'page';
1281
+ const pageName = names.has(`${stem}.txt`) ? `${n}-${stem}.txt` : `${stem}.txt`;
1282
+ names.add(pageName);
1283
+ staged.push({ label, name: pageName, buf: pageBuf, url: got.url });
1284
+ continue;
1285
+ }
1286
+ if (!isAbsolute(value)) return bad('input-not-absolute', n, `${value} must be the full path of a file, like /home/me/notes.md.`);
1287
+ const abs = resolve(value);
1288
+ let ls;
1289
+ try { ls = await lstat(abs); } catch { return bad('input-missing', n, `${abs} does not exist.`); }
1290
+ if (ls.isSymbolicLink()) return bad('input-symlink', n, `${abs} is a symlink — refused, never followed.`);
1291
+ if (!ls.isFile()) return bad('input-not-file', n, `${abs} is not a file — each input line is one file.`);
1292
+ const base = basename(abs);
1293
+ if (ENV_FILE.test(base)) return bad('input-env-file', n, `${abs} is an environment file — refused by name, whatever it contains.`);
1294
+ if (ls.size > RUBRIC_INPUT_MAX_BYTES) return bad('input-oversize', n, `${abs} is over the ${RUBRIC_INPUT_MAX_BYTES / (1024 * 1024)} MiB limit for a quoted input.`);
1295
+ const buf = await readFile(abs);
1296
+ try { utf8.decode(buf); } catch { return bad('input-not-utf8', n, `${abs} is not UTF-8 text — text files only for now.`); }
1297
+ if (hasNulByte(buf)) return bad('input-not-text', n, `${abs} is not a text file — text files only for now.`);
1298
+ // two files with one name keep the line number in front, so neither overwrites the other under input/
1299
+ const name = names.has(base) ? `${n}-${base}` : base;
1300
+ names.add(name);
1301
+ staged.push({ label, name, buf });
1302
+ }
1303
+ const stage = await mkdtemp(join(tmpdir(), 'bareloop-rubric-inputs-'));
1304
+ try {
1305
+ for (const f of staged) await writeFile(join(stage, f.name), f.buf);
1306
+ const prep = await prepareSource({ source: stage, into, ...(destination === undefined ? {} : { destination }) });
1307
+ if (prep.stop !== null) return { ok: false, code: prep.code, error: prep.stop };
1308
+ // `source` names the person's own first file, never the scratch staging folder that was already removed
1309
+ const manifest = {
1310
+ ...prep.manifest,
1311
+ source: isWebUrl(inputs[0].value) ? inputs[0].value.trim() : resolve(inputs[0].value),
1312
+ inputs: staged.map((f) => ({ label: f.label, rel: `input/${f.name}`, sha256: sha256Hex(f.buf), ...(f.url === undefined ? {} : { url: f.url }) })),
1313
+ };
1314
+ await writeFile(prep.manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
1315
+ return { ok: true, prep: { ...prep, manifest } };
1316
+ } finally {
1317
+ await rm(stage, { recursive: true, force: true });
1318
+ }
1319
+ }
1320
+
1321
+ /**
1322
+ * Is this Inputs value a path to an existing regular FILE on this machine (not a repo folder, not a web address, not a
1323
+ * missing path)? The $0 peek that tells a rubric job on local files from a rubric job on a repo before a session exists;
1324
+ * `proveRubricInputs` makes the authoritative, line-by-line proof.
1325
+ * @param {string} value
1326
+ * @returns {boolean}
1327
+ */
1328
+ export function looksLikeLocalFile(value) {
1329
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value) || !isAbsolute(value)) return false;
1330
+ try { return lstatSync(resolve(value)).isFile(); } catch { return false; }
1331
+ }
1332
+
1333
+ /**
1334
+ * Is this Inputs value one a rubric job can work on without a repo: a regular FILE on this machine, or a web address (PART 1c)?
1335
+ * @param {string} value
1336
+ * @returns {boolean}
1337
+ */
1338
+ export function looksLikeRubricInput(value) {
1339
+ return isWebUrl(value) || looksLikeLocalFile(value);
1340
+ }
1341
+
1342
+ /**
1343
+ * RUN START for a rubric job with web inputs (PART 1c, RF22): fetch every web input AGAIN and make that text the run's frozen input
1344
+ * BEFORE the worker starts. The drafting copy was only for the look (prices change); the close grades, and the worker reads, THIS
1345
+ * copy. It is written into the prepared tree, committed as the new seed (so the page is never read as a worker write), and
1346
+ * `source.json` is rewritten (new sha256 per input, new seed, `draftSha256` keeps what the look saw). Nothing but the text of a
1347
+ * recorded web address is ever refetched; a job with only file inputs returns at once and changes nothing.
1348
+ * @param {{into: string, fetchUrl?: (url: string) => Promise<{ok: true, text: string, url: string}|{ok: false, error: string}>}} o `into` - the prepared copy (source.json + tree/)
1349
+ * @returns {Promise<{ok: true, refreshed: string[], seed: string|null}|{ok: false, error: string}>}
1350
+ */
1351
+ export async function refreezeUrlInputs({ into, fetchUrl = (u) => fetchPageText(u) }) {
1352
+ const read = await readSourceManifest(into);
1353
+ if (read.stop !== null) return { ok: false, error: read.stop };
1354
+ if (!read.present || !read.manifest) return { ok: true, refreshed: [], seed: null };
1355
+ const manifest = read.manifest;
1356
+ const rows = Array.isArray(manifest.inputs) ? manifest.inputs.filter((/** @type {any} */ r) => r && typeof r.url === 'string' && r.url !== '') : [];
1357
+ if (rows.length === 0) return { ok: true, refreshed: [], seed: null };
1358
+ const tree = join(resolve(into), 'tree');
1359
+ /** @type {{row: any, buf: Buffer}[]} */
1360
+ const fresh = [];
1361
+ for (const row of rows) {
1362
+ const got = await fetchUrl(row.url);
1363
+ if (!got.ok) return { ok: false, error: `Input "${row.label}": ${got.error}. The run did not start. Nothing spent.` };
1364
+ const leak = secretPatternNames(got.text);
1365
+ if (leak.length) return { ok: false, error: `Input "${row.label}": the page at ${got.url} carries a known secret shape (${leak.join(', ')}) - refused, secrets never enter a job. The run did not start. Nothing spent.` };
1366
+ fresh.push({ row, buf: Buffer.from(got.text, 'utf8') });
1367
+ }
1368
+ for (const f of fresh) await writeFile(join(tree, f.row.rel), f.buf);
1369
+ const add = await git(tree, [...NO_HOOKS, 'add', '-A', '-f']);
1370
+ if (!add.ok) return { ok: false, error: `the fresh page copy could not be staged: ${add.err}. The run did not start. Nothing spent.` };
1371
+ const commit = await git(tree, [...GIT_IDENTITY, ...NO_HOOKS, 'commit', '-q', '--allow-empty', '-m', 'bareloop: run start - fresh copy of the web inputs']);
1372
+ if (!commit.ok) return { ok: false, error: `the fresh page copy could not be committed: ${commit.err}. The run did not start. Nothing spent.` };
1373
+ const head = await git(tree, ['rev-parse', 'HEAD']);
1374
+ if (!head.ok) return { ok: false, error: `git rev-parse HEAD failed in ${tree}: ${head.err}. The run did not start. Nothing spent.` };
1375
+ const seed = head.out.trim();
1376
+ const byLabel = new Map(fresh.map((f) => [f.row.label, f.buf]));
1377
+ const inputs = manifest.inputs.map((/** @type {any} */ r) => {
1378
+ const buf = byLabel.get(r.label);
1379
+ return buf === undefined || typeof r.url !== 'string' ? r : { ...r, draftSha256: r.draftSha256 ?? r.sha256, sha256: sha256Hex(buf) };
1380
+ });
1381
+ const files = Array.isArray(manifest.files) ? manifest.files.map((/** @type {any} */ m) => {
1382
+ const hit = inputs.find((/** @type {any} */ r) => typeof r.url === 'string' && r.rel === `input/${m.path}`);
1383
+ if (!hit) return m;
1384
+ const buf = byLabel.get(hit.label);
1385
+ return buf === undefined ? m : { ...m, bytes: buf.length, sha256: sha256Hex(buf) };
1386
+ }) : manifest.files;
1387
+ const next = { ...manifest, inputs, files, seed, refrozenAt: new Date().toISOString() };
1388
+ await writeFile(join(resolve(into), 'source.json'), `${JSON.stringify(next, null, 2)}\n`);
1389
+ return { ok: true, refreshed: rows.map((/** @type {any} */ r) => r.label), seed };
1390
+ }
package/src/u-readout.js CHANGED
@@ -202,7 +202,7 @@ export function reviewDoorPackage({ door, diff }) {
202
202
  const out = [];
203
203
  out.push(`REVIEW DOOR — this run's close minted ${String(d.outcome ?? 'a verdict')} and it STANDS. Nothing you do here changes it.`);
204
204
  if (held) {
205
- out.push(' held this green was rendered by the judged floor, so it has earned NO reuse and NO learning credit until you accept it');
205
+ out.push(' held this is a rubric (soft-green) job, so its green earns NO reuse and NO learning credit until you accept it, whether or not a judge ran');
206
206
  }
207
207
  // the body: what the machine judged, what moved, and the lines themselves —
208
208
  // rendered by the SAME code the pause screen uses, off the same record shape.
package/src/userrun.js CHANGED
@@ -42,7 +42,7 @@ import { makeSpine } from './spine.js';
42
42
  // signed into the job spec — a job is a repeatable SHAPE, the source/
43
43
  // destination a per-run value, exactly like `bareloop run <bundle> --repo
44
44
  // <path>` varies the repo without touching the bundle's signature).
45
- import { readSourceManifest, frontDoorFromManifest, proveDestination, copyOut } from './source.js';
45
+ import { readSourceManifest, frontDoorFromManifest, proveDestination, copyOut, refreezeUrlInputs } from './source.js';
46
46
  import { scanSecrets, redactSecrets } from './validate.js';
47
47
  import { runBehaviour, formatBehaviour } from './behaviour.js';
48
48
  // the three doors' SEMANTICS live in the library; this script surfaces them
@@ -235,6 +235,7 @@ class ExitSignal extends Error {
235
235
  * @property {any} [provider] the TEST SEAM: supplying this skips the real-key
236
236
  * check entirely, the same shape `src/cli.js`'s `main(argv, deps)` uses.
237
237
  * @property {(tier: string) => any} [providerFor]
238
+ * @property {(url: string) => Promise<{ok: true, text: string, url: string}|{ok: false, error: string}>} [fetchUrl] PART 1c - a test seam: how a web input is read at run start (production: barebrowse)
238
239
  * @property {any} [judgeProvider]
239
240
  * @property {string} [runlistHome] F196 — a test seam for `appendRun`'s
240
241
  * (src/runlist.js) `home` override, the same shape `deps.provider` already
@@ -2592,13 +2593,24 @@ export async function main(argv, deps = {}) {
2592
2593
  + 'with exactly one source-*/ sibling.');
2593
2594
  }
2594
2595
  const into = candidates[0];
2595
- const manifestRead = await readSourceManifest(into);
2596
+ let manifestRead = await readSourceManifest(into);
2596
2597
  if (manifestRead.stop !== null) {
2597
2598
  die(`--spec ${specPath}: ${manifestRead.code} — ${manifestRead.stop}`);
2598
2599
  }
2599
2600
  if (!manifestRead.present || !manifestRead.manifest) {
2600
2601
  die(`--spec ${specPath}: ${into} carries no source.json — not a prepared copy this runner can trust`);
2601
2602
  }
2603
+ // PART 1c (RF22): a FRESH run of a rubric job with web inputs fetches each address again and makes that text the run's frozen
2604
+ // input BEFORE the worker starts (the drafting copy was only for the look). A resume / door / decision keeps the copy its
2605
+ // first start froze. A refusal is plain words, $0.
2606
+ if (argFlag('resume') === null && argFlag('door') === null && argFlag('decide') === null) {
2607
+ const fresh = await refreezeUrlInputs({ into, ...(deps.fetchUrl ? { fetchUrl: deps.fetchUrl } : {}) });
2608
+ if (!fresh.ok) die(`--spec ${specPath}: ${fresh.error}`);
2609
+ if (fresh.refreshed.length > 0) {
2610
+ manifestRead = await readSourceManifest(into);
2611
+ if (manifestRead.stop !== null || !manifestRead.manifest) die(`--spec ${specPath}: the run copy's record could not be read after the fresh fetch`);
2612
+ }
2613
+ }
2602
2614
  const seed = manifestRead.manifest.seed;
2603
2615
  if (typeof seed !== 'string' || !seed) {
2604
2616
  die(`--spec ${specPath}: ${into}/source.json carries no seed commit — not a prepared copy this runner can trust`);
@@ -63,13 +63,16 @@ export function labelsFor(verdictType: string): Record<string | number, string>;
63
63
  * close — one source, never a second hand-typed list — rendered through the
64
64
  * frozen line in {@link GUARD_DESCRIPTIONS}, plus the write fence (a real,
65
65
  * signed fence — {@link writeScopeBlock}'s own subject) when one is set.
66
- * @param {{verdictType: string, lang: string, writeScope?: string[]|null}} o
66
+ * @param {{verdictType: string, lang: string, writeScope?: string[]|null, jobLines?: {n: number}[]|null}} o
67
67
  * @returns {string[]}
68
68
  */
69
- export function confirmProtections({ verdictType, lang, writeScope }: {
69
+ export function confirmProtections({ verdictType, lang, writeScope, jobLines }: {
70
70
  verdictType: string;
71
71
  lang: string;
72
72
  writeScope?: string[] | null;
73
+ jobLines?: {
74
+ n: number;
75
+ }[] | null;
73
76
  }): string[];
74
77
  /**
75
78
  * The mechanical seam between the catalogue and the schema, read BOTH ways: a
@@ -107,12 +110,15 @@ export function schemaCoverage(catalogue?: Record<string, any>): {
107
110
  * and failing loudly beats emitting a schema that quietly cannot say something
108
111
  * the validator will then demand.
109
112
  * `questions` (P7 item 5, last drafting try only) adds the optional `questions` property; absent, the schema is byte-identical.
113
+ * `kinds` (Part 1a) narrows the branches to the kinds a language may use (`catalogueForLang`) WITHOUT narrowing the coverage check,
114
+ * which stays over the whole catalogue; null (the default) = no narrowing.
110
115
  * @param {Record<string, any>} [catalogue]
111
- * @param {{verdictType?: string|null, questions?: boolean}} [o]
116
+ * @param {{verdictType?: string|null, questions?: boolean, kinds?: string[]|null}} [o]
112
117
  */
113
- export function declarationSchema(catalogue?: Record<string, any>, { verdictType, questions }?: {
118
+ export function declarationSchema(catalogue?: Record<string, any>, { verdictType, questions, kinds }?: {
114
119
  verdictType?: string | null;
115
120
  questions?: boolean;
121
+ kinds?: string[] | null;
116
122
  }): {
117
123
  type: string;
118
124
  properties: {
@@ -192,6 +198,11 @@ export function declarationSchema(catalogue?: Record<string, any>, { verdictType
192
198
  minimum: number;
193
199
  description: string;
194
200
  };
201
+ rule: {
202
+ type: string;
203
+ minimum: number;
204
+ description: string;
205
+ };
195
206
  reason: {
196
207
  type: string;
197
208
  minLength: number;
@@ -211,14 +222,15 @@ export function declarationSchema(catalogue?: Record<string, any>, { verdictType
211
222
  * The output channel. It records and acknowledges; it takes no action, which is
212
223
  * why handing it to a toolless authoring call does not reopen D3.
213
224
  * @param {{calls: any[]}} box
214
- * @param {{catalogue?: Record<string, any>, verdictType?: string|null, questions?: boolean}} [o]
225
+ * @param {{catalogue?: Record<string, any>, verdictType?: string|null, questions?: boolean, kinds?: string[]|null}} [o]
215
226
  */
216
227
  export function declarationTool(box: {
217
228
  calls: any[];
218
- }, { catalogue, verdictType, questions }?: {
229
+ }, { catalogue, verdictType, questions, kinds }?: {
219
230
  catalogue?: Record<string, any>;
220
231
  verdictType?: string | null;
221
232
  questions?: boolean;
233
+ kinds?: string[] | null;
222
234
  }): {
223
235
  name: string;
224
236
  description: string;
@@ -301,6 +313,11 @@ export function declarationTool(box: {
301
313
  minimum: number;
302
314
  description: string;
303
315
  };
316
+ rule: {
317
+ type: string;
318
+ minimum: number;
319
+ description: string;
320
+ };
304
321
  reason: {
305
322
  type: string;
306
323
  minLength: number;
@@ -400,6 +417,7 @@ export function writeScopeBlock(writeScope: string[]): string;
400
417
  * never merge.
401
418
  * @param {{checks: string[], checkItems?: {text: string, fromLine: number[]|null}[], protections: string[], openQuestions?: string[], notChecked?: string[],
402
419
  * answeredQuestions?: string[]}} confirmed
420
+ * @param {{numbered?: boolean}} [o] `numbered` (a rubric job on files): each confirmed check carries its `#n`, which a part cites in `covers`
403
421
  */
404
422
  export function confirmedBlock(confirmed: {
405
423
  checks: string[];
@@ -411,6 +429,8 @@ export function confirmedBlock(confirmed: {
411
429
  openQuestions?: string[];
412
430
  notChecked?: string[];
413
431
  answeredQuestions?: string[];
432
+ }, { numbered }?: {
433
+ numbered?: boolean;
414
434
  }): string;
415
435
  /**
416
436
  * P7: the job-line tagging order (model-facing, registered). Rule 1 of the job block: every stage the drafter writes
@@ -439,12 +459,14 @@ export function jobLineTagBlock(count: number): string;
439
459
  * @param {{answers: Record<string|number, string>, questions?: Record<string|number, string>,
440
460
  * facts: any, listingBlock: string, lang: string, verdictType: string,
441
461
  * guards: {name: string, kind: string, params: Record<string, any>, fill: string[]}[],
442
- * ownedEnvNames?: string[], mode?: 'tool'|'text', catalogue?: Record<string, any>,
462
+ * ownedEnvNames?: string[], mode?: 'tool'|'text', catalogue?: Record<string, any>|null,
443
463
  * writeScope?: string[]|null,
444
464
  * jobLines?: {n: number}[]|null, inputs?: {n: number, label: string, value: string}[]|null,
445
- * confirmed?: {checks: string[], checkItems?: {text: string, fromLine: number[]|null}[], protections: string[], openQuestions?: string[], notChecked?: string[], answeredQuestions?: string[]}|null}} o
465
+ * confirmed?: {checks: string[], checkItems?: {text: string, fromLine: number[]|null}[], protections: string[], openQuestions?: string[], notChecked?: string[], answeredQuestions?: string[]}|null,
466
+ * labelLists?: string[]|null,
467
+ * }} o
446
468
  */
447
- export function authorPrompt({ answers, questions, facts, listingBlock, lang, verdictType, guards, ownedEnvNames, mode, catalogue, writeScope, jobLines, inputs, confirmed, }: {
469
+ export function authorPrompt({ answers, questions, facts, listingBlock, lang, verdictType, guards, ownedEnvNames, mode, catalogue: catalogueIn, writeScope, jobLines, inputs, confirmed, labelLists, }: {
448
470
  answers: Record<string | number, string>;
449
471
  questions?: Record<string | number, string>;
450
472
  facts: any;
@@ -459,7 +481,7 @@ export function authorPrompt({ answers, questions, facts, listingBlock, lang, ve
459
481
  }[];
460
482
  ownedEnvNames?: string[];
461
483
  mode?: "tool" | "text";
462
- catalogue?: Record<string, any>;
484
+ catalogue?: Record<string, any> | null;
463
485
  writeScope?: string[] | null;
464
486
  jobLines?: {
465
487
  n: number;
@@ -480,6 +502,45 @@ export function authorPrompt({ answers, questions, facts, listingBlock, lang, ve
480
502
  notChecked?: string[];
481
503
  answeredQuestions?: string[];
482
504
  } | null;
505
+ labelLists?: string[] | null;
506
+ }): string;
507
+ /**
508
+ * The composer prompt for a rubric job on local files (Part 1a, docs/product/ITEM33-BUILD.md). There is no repository
509
+ * and no survey: the facts are the numbered inputs and the listing is the frozen tree (input/ files, output/). It states
510
+ * the step-file contract the worker is also told ({@link stepFileContract}), so the stage and the file cannot drift.
511
+ * @param {{answers: Record<string|number, string>, questions?: Record<string|number, string>, facts: any, listingBlock: string,
512
+ * verdictType: string, guards: {name: string, kind: string, params: Record<string, any>, fill: string[]}[],
513
+ * mode?: 'tool'|'text', catalogue?: Record<string, any>, writeScope?: string[]|null,
514
+ * jobLines?: {n: number}[]|null, inputs?: {n: number, label: string, value: string}[]|null, confirmed?: any,
515
+ * labelLists?: string[]|null,
516
+ * }} o
517
+ * @returns {string}
518
+ */
519
+ export function rubricAuthorPrompt({ answers, questions, facts, listingBlock, verdictType, guards, mode, catalogue, writeScope, jobLines, inputs, confirmed, labelLists, }: {
520
+ answers: Record<string | number, string>;
521
+ questions?: Record<string | number, string>;
522
+ facts: any;
523
+ listingBlock: string;
524
+ verdictType: string;
525
+ guards: {
526
+ name: string;
527
+ kind: string;
528
+ params: Record<string, any>;
529
+ fill: string[];
530
+ }[];
531
+ mode?: "tool" | "text";
532
+ catalogue?: Record<string, any>;
533
+ writeScope?: string[] | null;
534
+ jobLines?: {
535
+ n: number;
536
+ }[] | null;
537
+ inputs?: {
538
+ n: number;
539
+ label: string;
540
+ value: string;
541
+ }[] | null;
542
+ confirmed?: any;
543
+ labelLists?: string[] | null;
483
544
  }): string;
484
545
  /**
485
546
  * The seed read, as the model sees it: mechanical labels, no editorial. Per-stage
@@ -836,7 +897,8 @@ export function confirmPrompt({ answers, questions, labels, facts, listing, writ
836
897
  * generate: Function, book: ReturnType<typeof makeCostBook>,
837
898
  * ask: (step: {kind: string, [k: string]: any}) => Promise<string|null>,
838
899
  * onPhase?: (phase: string, data?: any) => void, mode?: 'tool'|'text',
839
- * worseThanBefore?: string, inputs?: {n: number, label: string, value: string}[]|null}} o retired field (hamr's ruling 2026-09-28):
900
+ * worseThanBefore?: string, inputs?: {n: number, label: string, value: string}[]|null,
901
+ * jobLines?: {n: number}[]|null}} o retired field (hamr's ruling 2026-09-28):
840
902
  * `worseThanBefore` is never asked any more and defaults to `''` — a
841
903
  * caller may still pass a string through (kept for a future, non-ask
842
904
  * source of the same constraint), it just never comes from an ask() step.
@@ -847,7 +909,7 @@ export function confirmPrompt({ answers, questions, labels, facts, listing, writ
847
909
  * worseThanBefore: string, openQuestions: string[], notChecked: string[], answeredQuestions?: string[]}|null,
848
910
  * reds: Red[], cost: any}>}
849
911
  */
850
- export function runConfirmTurn({ verdictType, answers, questions, labels, facts, listing, writeScope, isRepo, lang, generate, book, ask, onPhase, mode, worseThanBefore: presetWorseThanBefore, inputs, }: {
912
+ export function runConfirmTurn({ verdictType, answers, questions, labels, facts, listing, writeScope, isRepo, lang, generate, book, ask, onPhase, mode, worseThanBefore: presetWorseThanBefore, inputs, jobLines, }: {
851
913
  verdictType: string;
852
914
  answers: Record<string | number, string>;
853
915
  questions: Record<string | number, string>;
@@ -875,6 +937,9 @@ export function runConfirmTurn({ verdictType, answers, questions, labels, facts,
875
937
  label: string;
876
938
  value: string;
877
939
  }[] | null;
940
+ jobLines?: {
941
+ n: number;
942
+ }[] | null;
878
943
  }): Promise<{
879
944
  ok: boolean;
880
945
  stop: null | "cap-halt" | "pricing-red" | "provider-red" | "artifact-red" | "confirm-abandoned" | "confirm-restart";
@@ -945,7 +1010,7 @@ export function runConfirmTurn({ verdictType, answers, questions, labels, facts,
945
1010
  * offerQuestions?: boolean,
946
1011
  * confirmed?: {checks: string[], checkItems?: {text: string, fromLine: number[]|null}[], protections: string[], openQuestions?: string[], notChecked?: string[], answeredQuestions?: string[]}|null}} o
947
1012
  */
948
- export function authorClose({ workdir, seedRef, lang, verdictType, answers, questions, writeScope, scout, listing, priorCalls, priorRaws, confirmed, jobLines, inputs, generate, seedReadFn, closeCtx, ceilingUsd, onPhase, onCall, maxRevisions, structureRetries, structuredMode, catalogue, offerQuestions, }: {
1013
+ export function authorClose({ workdir, seedRef, lang, verdictType, answers, questions, writeScope, scout, listing, priorCalls, priorRaws, confirmed, jobLines, inputs, generate, seedReadFn, closeCtx, ceilingUsd, onPhase, onCall, maxRevisions, structureRetries, structuredMode, catalogue: catalogueIn, offerQuestions, }: {
949
1014
  workdir: string;
950
1015
  seedRef: string;
951
1016
  lang: string;
@@ -1522,12 +1587,195 @@ export const PARAM_SCHEMAS: Readonly<{
1522
1587
  };
1523
1588
  description: string;
1524
1589
  };
1590
+ step: {
1591
+ type: string;
1592
+ minimum: number;
1593
+ description: string;
1594
+ };
1595
+ parts: {
1596
+ type: string;
1597
+ description: string;
1598
+ items: {
1599
+ type: string;
1600
+ properties: {
1601
+ text: {
1602
+ type: string;
1603
+ minLength: number;
1604
+ description: string;
1605
+ };
1606
+ class: {
1607
+ enum: string[];
1608
+ };
1609
+ check: {
1610
+ enum: string[];
1611
+ description: string;
1612
+ };
1613
+ value: {
1614
+ type: string;
1615
+ minimum: number;
1616
+ description: string;
1617
+ };
1618
+ names: {
1619
+ type: string;
1620
+ minItems: number;
1621
+ items: {
1622
+ type: string;
1623
+ minLength: number;
1624
+ };
1625
+ description: string;
1626
+ };
1627
+ wordsPerSection: {
1628
+ type: string;
1629
+ minimum: number;
1630
+ };
1631
+ source: {
1632
+ type: string;
1633
+ minLength: number;
1634
+ description: string;
1635
+ };
1636
+ domains: {
1637
+ type: string;
1638
+ minItems: number;
1639
+ items: {
1640
+ type: string;
1641
+ minLength: number;
1642
+ };
1643
+ description: string;
1644
+ };
1645
+ covers: {
1646
+ type: string;
1647
+ minItems: number;
1648
+ uniqueItems: boolean;
1649
+ items: {
1650
+ type: string;
1651
+ minimum: number;
1652
+ };
1653
+ description: string;
1654
+ };
1655
+ rules: {
1656
+ type: string;
1657
+ minItems: number;
1658
+ uniqueItems: boolean;
1659
+ items: {
1660
+ type: string;
1661
+ minimum: number;
1662
+ };
1663
+ description: string;
1664
+ };
1665
+ facts: {
1666
+ type: string;
1667
+ minItems: number;
1668
+ maxItems: number;
1669
+ description: string;
1670
+ items: {
1671
+ type: string;
1672
+ properties: {
1673
+ name: {
1674
+ type: string;
1675
+ minLength: number;
1676
+ description: string;
1677
+ };
1678
+ shape: {
1679
+ enum: string[];
1680
+ description: string;
1681
+ };
1682
+ unit: {
1683
+ type: string;
1684
+ minLength: number;
1685
+ description: string;
1686
+ };
1687
+ currency: {
1688
+ enum: string[];
1689
+ description: string;
1690
+ };
1691
+ phrase: {
1692
+ type: string;
1693
+ minLength: number;
1694
+ description: string;
1695
+ };
1696
+ anchor: {
1697
+ type: string;
1698
+ minLength: number;
1699
+ description: string;
1700
+ };
1701
+ until: {
1702
+ type: string;
1703
+ minLength: number;
1704
+ description: string;
1705
+ };
1706
+ rule: {
1707
+ type: string;
1708
+ description: string;
1709
+ properties: {
1710
+ op: {
1711
+ enum: string[];
1712
+ };
1713
+ value: {
1714
+ type: string[];
1715
+ description: string;
1716
+ };
1717
+ lo: {
1718
+ type: string[];
1719
+ };
1720
+ hi: {
1721
+ type: string[];
1722
+ };
1723
+ };
1724
+ required: string[];
1725
+ additionalProperties: boolean;
1726
+ };
1727
+ aiProposal: {
1728
+ type: string;
1729
+ description: string;
1730
+ };
1731
+ };
1732
+ required: string[];
1733
+ additionalProperties: boolean;
1734
+ };
1735
+ };
1736
+ count: {
1737
+ type: string;
1738
+ description: string;
1739
+ properties: {
1740
+ min: {
1741
+ type: string;
1742
+ minimum: number;
1743
+ };
1744
+ max: {
1745
+ type: string;
1746
+ minimum: number;
1747
+ };
1748
+ };
1749
+ additionalProperties: boolean;
1750
+ };
1751
+ };
1752
+ required: string[];
1753
+ additionalProperties: boolean;
1754
+ };
1755
+ };
1525
1756
  ask: {
1526
1757
  type: string;
1527
1758
  minLength: number;
1528
1759
  description: string;
1529
1760
  };
1530
1761
  }>;
1762
+ /**
1763
+ * What a rubric job on local files means by DONE — the class statement of the text genre (Part 1a). It is stated as an order
1764
+ * about KINDS: every part of a job line is counted or checked against a quote, because nothing in this build judges.
1765
+ */
1766
+ export const RUBRIC_FILES_STATEMENT: string;
1767
+ /**
1768
+ * THE RUBRIC RULES, in one place (live fix B1): what the confirm step and the drafter are both told about turning the person's words
1769
+ * into checks. The confirm note ({@link RUBRIC_CONFIRM_NOTE}) and the composer brief ({@link REQUIRED_FACTS_BRIEF}) interpolate THIS
1770
+ * constant, so the two cannot drift (in smv6k6l8dxkzs the confirm step planned a limit the drafter then could not carry, and the
1771
+ * drafter added an airport code the person never wrote). Field-neutral: neither turn needs the other's field names.
1772
+ */
1773
+ export const RUBRIC_RULES: string;
1774
+ /**
1775
+ * REQUIRED FACTS, as the composer is told them (PART 1b, PRD item 33 RF1–RF17). The shape of the fact part is in the catalogue and the
1776
+ * schema; this is the judgement the schema cannot carry: what becomes a fact, where its words come from, and what is Not checked.
1777
+ */
1778
+ export const REQUIRED_FACTS_BRIEF: string;
1531
1779
  export const CONFIRM_TOOL_NAME: "confirm_plan";
1532
1780
  /** Acknowledgement text the confirm tool's `execute` returns — mirrors
1533
1781
  * {@link DECLARATION_ACK}'s role: an output channel that records and
@@ -1545,6 +1793,12 @@ export const CONFIRM_STRUCTURE_INSTRUCTION: string;
1545
1793
  * an ORDER to the model itself, the same register {@link AUTHOR_SYSTEM} and
1546
1794
  * {@link CLASS_STATEMENTS} already use, rather than a code matcher over prose.
1547
1795
  */
1796
+ /**
1797
+ * What the confirm turn tells the model about a rubric job on local text files (model-facing, registered). There is no
1798
+ * repository and no judge: a check is a limit a machine counts (words, lines, sections), or a claim a quote from a named
1799
+ * input must back. Everything else the person asked for is named in `notChecked`, in their words.
1800
+ */
1801
+ export const RUBRIC_CONFIRM_NOTE: string;
1548
1802
  export const CONFIRM_SYSTEM: string;
1549
1803
  export type Red = {
1550
1804
  code: string;