@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.
- package/CHANGELOG.md +60 -0
- package/README.md +2 -2
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/fold-completeness.md +7 -3
- package/references/modes/review-ledger.md +3 -2
- package/tools/commands.mjs +2 -2
- package/tools/fold-completeness-run.mjs +360 -37
- package/tools/fold-completeness.mjs +247 -57
- package/tools/review-ledger-write.mjs +60 -6
- package/tools/review-ledger.mjs +69 -14
- package/tools/seed-gates.mjs +2 -2
package/tools/review-ledger.mjs
CHANGED
|
@@ -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
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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 (
|
|
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');
|
package/tools/seed-gates.mjs
CHANGED
|
@@ -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 /
|
|
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
|
|
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,
|