bareloop 0.8.0 → 0.10.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/src/job.js CHANGED
@@ -15,6 +15,7 @@
15
15
 
16
16
  import { createHash } from 'node:crypto';
17
17
  import { globToPrefix, scopeContained, isObj, isNonEmptyString, sweepSecretLiterals, hasNestedQuantifier } from './validate.js';
18
+ import { validateCloseDecl, DECLARED_CLOSE_CLASSES } from './declaredclose.js';
18
19
 
19
20
  // The menus below ARE the close-authoring hierarchy (PRD §7) and ship frozen:
20
21
  // they are read at call time, so a mutable export would let adopter code
@@ -25,7 +26,20 @@ export const CLOSE_TYPES = Object.freeze(['predicate', 'gold', 'rubric', 'hitl']
25
26
  * type cannot legally claim (green-on-rubric is laundering). The `CLASSES`
26
27
  * vocabulary export went with steps[] (PRD v1.32): a class was a STEP field, and
27
28
  * nothing declares one now. */
28
- export const CLASS_BY_CLOSE = Object.freeze({ predicate: Object.freeze(['hard']), gold: Object.freeze(['hard']), rubric: Object.freeze(['soft']), hitl: Object.freeze(['hitl']) });
29
+ export const CLASS_BY_CLOSE = Object.freeze({
30
+ predicate: Object.freeze(['hard']),
31
+ gold: Object.freeze(['hard']),
32
+ rubric: Object.freeze(['soft']),
33
+ hitl: Object.freeze(['hitl']),
34
+ // The AUTHORED close (M4, `closeDecl`) is not a `close.type` — it is the other
35
+ // field a plan-shape spec may carry — but it claims a verdict class exactly
36
+ // like one, so it belongs in the SAME table rather than in a second constant
37
+ // beside it. Hard by construction: every live kind is a mechanical command
38
+ // whose measurement is the truth, and the two judgment kinds are locked out at
39
+ // the catalogue and absent from the executor. The suite pins the key set
40
+ // against CLOSE_TYPES so the two can never drift apart silently.
41
+ declared: DECLARED_CLOSE_CLASSES,
42
+ });
29
43
  export const GOLD_COMPARE = Object.freeze(['exact', 'json-equal']);
30
44
  /** the tool grant menu (2b interview #1): read/grep/write, plus the two litectx
31
45
  * RETRIEVAL verbs (F19) — `run` stays locked-but-listed; a spec requesting it
@@ -108,11 +122,11 @@ export const CONDITION_KEYS = Object.freeze(['providerPath', 'closeVerbosity', '
108
122
  // `steps` stays in the field list ONLY so a retired spec reds by name
109
123
  // (`shape-retired`) instead of falling through to a generic unknown-field —
110
124
  // the operator gets told what happened, not just that something is wrong.
111
- const JOB_FIELDS = ['schema', 'job', 'description', 'provider', 'conditions', 'cadence', 'budgetUsd', 'maxWallMs', 'writeScope', 'steps', 'escalation', 'goal', 'verdictType', 'close', 'checks', 'tools'];
125
+ const JOB_FIELDS = ['schema', 'job', 'description', 'provider', 'conditions', 'cadence', 'budgetUsd', 'maxWallMs', 'writeScope', 'steps', 'escalation', 'goal', 'verdictType', 'close', 'closeDecl', 'checks', 'tools'];
112
126
  /** the four-field plan shape's core (decision 5) — presence of any of these
113
127
  * declares the shape; `tools` (the ceiling) rides the shape but alone does not
114
128
  * declare it, so a legacy spec carrying it gets a pointed red, not a conflict */
115
- const PLAN_CORE_FIELDS = ['goal', 'verdictType', 'close', 'checks'];
129
+ const PLAN_CORE_FIELDS = ['goal', 'verdictType', 'close', 'closeDecl', 'checks'];
116
130
  /** exact field set per close type — anything else is an unknown-field red
117
131
  * (freeform code, script bodies, and minting claims all land there) */
118
132
  const CLOSE_FIELDS = {
@@ -530,7 +544,37 @@ function validatePlanShape(spec, red, reds) {
530
544
  demanded = CLASS_BY_VERDICT[spec.verdictType];
531
545
  }
532
546
 
533
- if (spec.close === undefined) red('missing-required', 'close', 'declared green with nothing to run it — preflight validates the declaration, never infers');
547
+ // THE TWO CLOSE FIELDS, and they are ALTERNATIVES (M4). `close` is the
548
+ // operator's own command stages; `closeDecl` is the AUTHORED declaration over
549
+ // owned kinds — the whole point of the close-authoring rung, and the field a
550
+ // user who is not an engineer ends up with. Both at once is refused rather
551
+ // than merged or precedence-ordered: a spec carrying two closes has two
552
+ // arbiters, and picking one silently is how the signed artefact stops meaning
553
+ // what the signer read.
554
+ if (spec.close !== undefined && spec.closeDecl !== undefined) {
555
+ red('close-duplicated', 'closeDecl', 'a job declares EITHER a command close (close) OR an authored declaration '
556
+ + '(closeDecl) — never both. Two closes are two arbiters, and the close is the only truth');
557
+ } else if (spec.closeDecl !== undefined) {
558
+ // The DECLARED close. The tree-grounded half of the gate (hamr's listing
559
+ // rule, and the scoped-job derivation that arms the F84 one-population law)
560
+ // is DEFERRED here and re-run by the runner against the real seed before any
561
+ // stage executes — a job spec is validated with no repository in hand, and a
562
+ // validator that quietly skipped those two rules would report a declaration
563
+ // it never examined. `grounded: false` is what the runner keys on.
564
+ // the class rides along: the mandatory guard battery keys off the verdict
565
+ // class the user picked (PRD v1.57 §2), so the gate cannot check D5 without
566
+ // it. An unknown or locked class is refused there, on its own axis, beside
567
+ // the counted `request-red` above.
568
+ const cd = validateCloseDecl(spec.closeDecl, { at: 'closeDecl', deferListing: true, verdictType: spec.verdictType });
569
+ for (const r of cd.reds) reds.push(r);
570
+ if (demanded !== undefined && !CLASS_BY_CLOSE.declared.includes(demanded)) {
571
+ // the same laundering guard as the close-type hierarchy, one level up: a
572
+ // declared close is HARD, so a soft/hitl verdict can never ride one
573
+ red('close-hierarchy', 'verdictType', `verdictType ${spec.verdictType} demands a ${demanded}-class close; an `
574
+ + `authored declaration admits ${CLASS_BY_CLOSE.declared.join('|')} only — every live kind is a mechanical `
575
+ + 'measurement, and the two judgment kinds are locked out of the catalogue (PRD §7)');
576
+ }
577
+ } else if (spec.close === undefined) red('missing-required', 'close', 'declared green with nothing to run it — preflight validates the declaration, never infers');
534
578
  // The staged close (PRD v1.28): an ordered list of named command stages, from
535
579
  // which the check menu derives. The object form remains ONLY for the
536
580
  // declared-but-locked verdict classes (gold/rubric/hitl), which name no