@sabaiway/agent-workflow-kit 1.37.0 → 1.38.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.
@@ -71,13 +71,15 @@ const SLOT = 'review';
71
71
  // there, never here (it is not a decideStop input).
72
72
  export const REVIEW_CAP = 2;
73
73
  // SCHEMA_VERSION is what the WRITER emits (M2/AD-046: a fixable-bug triage requires a non-null,
74
- // well-formed testId — the red→green test that pins the fold). The READER tolerates every
75
- // SUPPORTED_SCHEMAS version under its own per-version rules, so historical/live v1 ledgers never
76
- // retroactively become malformed (Decision 2 a malformed line cascades fail-closed refusals in the
77
- // writer teeth AND the --check gate). v1 records keep the AD-045 rule (testId optional, unenforced);
78
- // only v2 enforces the test-per-fold binding. decideStop never reads testId (not a decideStop input).
79
- export const SCHEMA_VERSION = 2;
80
- const SUPPORTED_SCHEMAS = new Set([1, 2]);
74
+ // well-formed testId — the red→green test that pins the fold; BUGFREE-1/AD-047: v3 adds the
75
+ // `override` record kind the loud, durable waiver the fold-completeness gate consumes). The
76
+ // READER tolerates every SUPPORTED_SCHEMAS version under its own per-version rules, so
77
+ // historical/live v1/v2 ledgers never retroactively become malformed (Decision 2 a malformed line
78
+ // cascades fail-closed refusals in the writer teeth AND the --check gate). v1 records keep the
79
+ // AD-045 rule (testId optional, unenforced); v2+ enforces the test-per-fold binding; ONLY v3 may
80
+ // carry kind `override`. decideStop never reads testId or overrides (not decideStop inputs).
81
+ export const SCHEMA_VERSION = 3;
82
+ const SUPPORTED_SCHEMAS = new Set([1, 2, 3]);
81
83
 
82
84
  // The record vocabulary — the single home of every enum the schema validates.
83
85
  const ACTIVITIES_SET = new Set(['plan-authoring', 'plan-execution']);
@@ -85,6 +87,7 @@ const KINDS_SET = new Set(['round', 'triage']);
85
87
  const SEVERITIES = new Set(['blocker', 'major', 'minor']);
86
88
  export const ORIGINS = ['first-draft', 'fold-induced', 'mechanics'];
87
89
  const CLASSES = new Set(['fixable-bug', 'inherent-layer-residual', 'escalate']);
90
+ const OVERRIDE_SCOPES = new Set(['oracle-change', 'red-proof']);
88
91
 
89
92
  // ── git-dir resolution (read-only queries; the ledger lives in the git dir, uncommittable) ──────
90
93
 
@@ -122,13 +125,19 @@ const isNonNegInt = (v) => Number.isInteger(v) && v >= 0;
122
125
  // testId FORMAT (Decision 3): "<repo-relative test file>#<test-name-pattern>" — a "#" separator with
123
126
  // BOTH halves non-empty. NO file-suffix rule: a suffix check would itself be a special case and would
124
127
  // block a consumer's own naming (e.g. `.spec.js`; agy R1). The reader validates FORMAT only (it stays
125
- // hermetic); the fold-completeness gate validates RESOLVABILITY via a bound-test probe run.
128
+ // hermetic); the fold-completeness gate validates RESOLVABILITY via a bound-test probe run. Exported
129
+ // (with the splitter) as the single home of the format — the fold-completeness pair validates and
130
+ // splits testIds through THESE, so the format can never fork (BUGFREE-1 / AD-047).
126
131
  const TESTID_SEPARATOR = '#';
127
- const isWellFormedTestId = (v) => {
132
+ export const isWellFormedTestId = (v) => {
128
133
  if (typeof v !== 'string') return false;
129
134
  const at = v.indexOf(TESTID_SEPARATOR);
130
135
  return at > 0 && at < v.length - 1; // separator present, both halves non-empty
131
136
  };
137
+ export const splitTestId = (v) => {
138
+ const at = v.indexOf(TESTID_SEPARATOR);
139
+ return { file: v.slice(0, at), pattern: v.slice(at + 1) };
140
+ };
132
141
 
133
142
  // validateRound(obj) → { ok, reason }. Structural checks + the two internal-consistency invariants:
134
143
  // the per-backend findings-by-severity equal that backend's counts, and the origins tally equals the
@@ -203,19 +212,47 @@ const validateTriage = (obj, schema = SCHEMA_VERSION) => {
203
212
  return { ok: true };
204
213
  };
205
214
 
215
+ // validateOverride(obj) → { ok, reason }. v3 only (BUGFREE-1 / AD-047, D3): scope `oracle-change`
216
+ // carries non-empty repo-relative files[] + reason; scope `red-proof` carries a REQUIRED
217
+ // well-formed testId + reason, no files[]. Payloads are EXACT — a stray cross-scope field is a
218
+ // forgery smell, rejected. Loop + payload scoped; the fingerprint is recorded for audit only.
219
+ const OVERRIDE_SHARED_KEYS = new Set(['schema', 'loop', 'activity', 'kind', 'round', 'fingerprint', 'timestamp', 'scope', 'reason']);
220
+ const validateOverride = (obj) => {
221
+ if (!OVERRIDE_SCOPES.has(obj.scope)) return { ok: false, reason: `override: bad scope "${obj.scope}" (expected oracle-change | red-proof)` };
222
+ // EXACT per-scope payloads via an allow-list (codex R5): a stray key — a cross-scope field or an
223
+ // arbitrary hand-added one — is a forgery smell, rejected by name (the mutation-shape precedent).
224
+ const payloadKey = obj.scope === 'oracle-change' ? 'files' : 'testId';
225
+ for (const k of Object.keys(obj)) {
226
+ if (!OVERRIDE_SHARED_KEYS.has(k) && k !== payloadKey) return { ok: false, reason: `override: unknown key "${k}" (exact per-scope payloads: shared frame + ${payloadKey})` };
227
+ }
228
+ if (!isNonEmptyString(obj.reason)) return { ok: false, reason: 'override: a non-empty reason is required (never a silent waiver)' };
229
+ if (obj.scope === 'oracle-change') {
230
+ if (!Array.isArray(obj.files) || obj.files.length === 0) return { ok: false, reason: 'override: oracle-change files[] must be a non-empty array' };
231
+ for (const f of obj.files) {
232
+ if (!isNonEmptyString(f) || f.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(f)) return { ok: false, reason: `override: files[] entries must be non-empty repo-relative paths (got ${JSON.stringify(f)})` };
233
+ }
234
+ return { ok: true };
235
+ }
236
+ if (!isWellFormedTestId(obj.testId)) return { ok: false, reason: 'override: a red-proof override requires a well-formed testId "<test-file>#<test-name-pattern>"' };
237
+ return { ok: true };
238
+ };
239
+
206
240
  // validateRecord(obj) → { ok, reason }. The shared frame (schema/loop/activity/kind/round/
207
241
  // fingerprint/timestamp) then the per-kind body. `reason` names the exact failed check so the
208
- // malformed-line surface and the per-check named tests can assert it.
242
+ // malformed-line surface and the per-check named tests can assert it. Kind vocabulary is
243
+ // per-version: `override` exists only under schema >= 3 (older records never grow new kinds).
209
244
  export const validateRecord = (obj) => {
210
245
  if (!isPlainObject(obj)) return { ok: false, reason: 'not an object' };
211
246
  if (!SUPPORTED_SCHEMAS.has(obj.schema)) return { ok: false, reason: `schema must be one of ${[...SUPPORTED_SCHEMAS].join(', ')}` };
212
247
  if (!isNonEmptyString(obj.loop)) return { ok: false, reason: 'missing loop' };
213
248
  if (!ACTIVITIES_SET.has(obj.activity)) return { ok: false, reason: `bad activity "${obj.activity}"` };
214
- if (!KINDS_SET.has(obj.kind)) return { ok: false, reason: `bad kind "${obj.kind}"` };
249
+ if (!KINDS_SET.has(obj.kind) && !(obj.schema >= 3 && obj.kind === 'override')) return { ok: false, reason: `bad kind "${obj.kind}"` };
215
250
  if (!(Number.isInteger(obj.round) && obj.round >= 1)) return { ok: false, reason: 'round must be an integer >= 1' };
216
251
  if (!(obj.fingerprint === null || isNonEmptyString(obj.fingerprint))) return { ok: false, reason: 'fingerprint must be null or a non-empty string' };
217
252
  if (!isNonEmptyString(obj.timestamp)) return { ok: false, reason: 'missing timestamp' };
218
- return obj.kind === 'round' ? validateRound(obj) : validateTriage(obj, obj.schema);
253
+ if (obj.kind === 'round') return validateRound(obj);
254
+ if (obj.kind === 'override') return validateOverride(obj);
255
+ return validateTriage(obj, obj.schema);
219
256
  };
220
257
 
221
258
  // readLedger(path) → { records, malformed, malformedReasons }. Absent file → empty (no review ran).
@@ -249,12 +286,27 @@ export const readLedger = (path, readFile = readFileSync) => {
249
286
  return { records, malformed: malformedReasons.length, malformedReasons };
250
287
  };
251
288
 
252
- // filterLoopRecords(records, { activity, loop }) → the records of ONE loop (both kinds), order
289
+ // filterLoopRecords(records, { activity, loop }) → the records of ONE loop (all kinds), order
253
290
  // preserved. The gate filters to activity==="plan-execution" AND loop===the in-flight plan stem;
254
291
  // authoring rounds (and other plans' rounds) never enter the code gate.
255
292
  export const filterLoopRecords = (records, { activity, loop }) =>
256
293
  records.filter((r) => r.activity === activity && r.loop === loop);
257
294
 
295
+ // collectOverrides(records, { activity, loop }) → { oracleChangeFiles: Set, redProofTestIds: Set }.
296
+ // The UNION of the loop's recorded override payloads — loop + payload scoped, never fingerprint-
297
+ // bound (D3: re-affirmation churn on every later edit would train rubber-stamping). The
298
+ // fold-completeness checker consumes THIS (both modules are read-only — the read/write split holds).
299
+ export const collectOverrides = (records, { activity = ACTIVITY, loop } = {}) => {
300
+ const oracleChangeFiles = new Set();
301
+ const redProofTestIds = new Set();
302
+ for (const r of filterLoopRecords(records, { activity, loop })) {
303
+ if (r.kind !== 'override') continue;
304
+ if (r.scope === 'oracle-change') for (const f of r.files) oracleChangeFiles.add(f);
305
+ else redProofTestIds.add(r.testId);
306
+ }
307
+ return { oracleChangeFiles, redProofTestIds };
308
+ };
309
+
258
310
  // roundSequenceIntact(records) → true iff the round records, in file order, number exactly 1,2,…,n
259
311
  // (no duplicate, gap, or out-of-order round). Checks the EXISTING sequence, not just the incoming
260
312
  // round: a ledger like [2] / [1,1] / [2,1] (reachable only by hand-editing the git-dir file — the
@@ -459,6 +511,9 @@ const roundLine = (r) => {
459
511
  const triageLine = (t) =>
460
512
  ` triage @round ${t.round} — ${t.classifications.map((c) => `${c.findingKey}=${c.class}${c.class === 'escalate' ? `(accepted:${c.accepted})` : ''}`).join(', ')}`;
461
513
 
514
+ const overrideLine = (o) =>
515
+ ` override @round ${o.round} [${o.scope}] — ${o.scope === 'oracle-change' ? o.files.join(', ') : o.testId}: ${o.reason}`;
516
+
462
517
  const formatHuman = (state, check) => {
463
518
  const lines = [
464
519
  `review-ledger — ${ACTIVITY}.${SLOT} = ${state.resolved.recipe} (${state.resolved.source === 'config' ? `from ${CONFIG_REL}` : 'computed default'})${state.requiredBackends.length ? ` → ${state.requiredBackends.join(' + ')}` : ''}`,
@@ -472,7 +527,7 @@ const formatHuman = (state, check) => {
472
527
  if (state.plans.length === 1) {
473
528
  const loop = state.plans[0].replace(/\.md$/, '');
474
529
  const forLoop = filterLoopRecords(state.records, { activity: ACTIVITY, loop });
475
- for (const r of forLoop) lines.push(r.kind === 'round' ? roundLine(r) : triageLine(r));
530
+ for (const r of forLoop) lines.push(r.kind === 'round' ? roundLine(r) : r.kind === 'override' ? overrideLine(r) : triageLine(r));
476
531
  }
477
532
  lines.push(` check: ${check.code === 0 ? 'PASS' : 'FAIL'} — ${check.reason}`);
478
533
  return lines.join('\n');
@@ -208,7 +208,7 @@ export const reviewStateCandidate = (cwd, deps = {}) => {
208
208
 
209
209
  // The conditional review-LEDGER candidate (AD-045) — the SAME consent + conditional rule as the
210
210
  // review-state candidate (offered ONLY when plan-execution.review is reviewed/council), keyed on the
211
- // same slot, path resolved + QUOTED. It gates the review-ROUND ledger (converged / accepted-residual);
211
+ // same slot, path resolved + QUOTED. It gates the review-ROUND ledger (converged / resolved-residual);
212
212
  // review-state gates receipt PRESENCE. Both may be offered together — distinct axes.
213
213
  export const reviewLedgerCandidate = (cwd, deps = {}) => {
214
214
  const toolPath = deps.reviewLedgerTool ?? REVIEW_LEDGER_TOOL;
@@ -228,7 +228,7 @@ export const reviewLedgerCandidate = (cwd, deps = {}) => {
228
228
  return {
229
229
  candidate: {
230
230
  id: 'review-ledger',
231
- title: 'Review-round ledger: the in-flight loop is converged or accepted-residual',
231
+ title: 'Review-round ledger: the in-flight loop is converged or resolved-residual',
232
232
  cmd: `node "${toolPath}" --check`,
233
233
  },
234
234
  note: null,