@sabaiway/agent-workflow-kit 5.5.0 → 5.6.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 +62 -0
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/gates.md +6 -3
- package/references/modes/procedures.md +2 -0
- package/references/modes/recommendations.md +1 -1
- package/references/modes/velocity.md +1 -0
- package/tools/flow-check-cores.mjs +253 -0
- package/tools/flow-check-git-lane.mjs +56 -0
- package/tools/flow-check-rungs.mjs +330 -0
- package/tools/flow-check.mjs +23 -611
- package/tools/gates-declaration.mjs +13 -1
- package/tools/gates-init.mjs +134 -22
- package/tools/procedures.mjs +64 -5
- package/tools/recommendations.mjs +108 -7
- package/tools/source-size-check.mjs +320 -0
- package/tools/source-size-config.mjs +244 -0
- package/tools/source-size-core.mjs +53 -0
- package/tools/source-size-gate-cmd.mjs +55 -0
- package/tools/source-size-judge.mjs +114 -0
- package/tools/source-size-refusal.mjs +70 -0
- package/tools/source-size-report.mjs +254 -0
- package/tools/source-size-scope.mjs +145 -0
- package/tools/velocity-profile.mjs +24 -3
|
@@ -11,6 +11,7 @@ import { join, isAbsolute } from 'node:path';
|
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { fail, loadConfig, CONFIG_REL } from './orchestration-config.mjs';
|
|
13
13
|
import { matchesCoverageProducer } from './coverage-producer.mjs';
|
|
14
|
+
import { matchesSourceSizeGate } from './source-size-core.mjs';
|
|
14
15
|
|
|
15
16
|
// The per-project declaration (strict JSON, hand-editable). cwd-relative — errors show a path the
|
|
16
17
|
// user can open (the orchestration-config CONFIG_REL idiom).
|
|
@@ -174,7 +175,8 @@ export const coverageDeclarationDefects = (gates, projectDir) => {
|
|
|
174
175
|
message:
|
|
175
176
|
`${GATES_REL}: the canonical coverage checker (${checkers[0].id}) must be the LAST declared gate — ` +
|
|
176
177
|
`${after.map((g) => g.id).join(', ')} would run after it consumed the lcov. REORDER the declaration by hand ` +
|
|
177
|
-
'(the gate itself is fine — this is an ORDERING refusal
|
|
178
|
+
'(the gate itself is fine — this is an ORDERING refusal about entries that are ALREADY declared: the fill ' +
|
|
179
|
+
'places a new entry before a trailing checker, but it never reorders what it did not write)',
|
|
178
180
|
}];
|
|
179
181
|
}
|
|
180
182
|
if (!coverageProducerPrecedes(gates, index)) {
|
|
@@ -202,6 +204,16 @@ const REVIEW_DEPENDENT_CHECKS = ['review-state', 'commit-guard', 'coverage-check
|
|
|
202
204
|
export const isReviewDependentGate = (gate, projectDir) =>
|
|
203
205
|
REVIEW_DEPENDENT_CHECKS.some((check) => matchesCanonicalCheck(check, gate.cmd, projectDir));
|
|
204
206
|
|
|
207
|
+
// isKitOwnedCheckerGate — is this gate one of the KIT's own checkers, rather than something the
|
|
208
|
+
// project declared to verify itself? A separate question from review-dependence, and the two stopped
|
|
209
|
+
// coinciding the moment a kit checker arrived that needs no receipt: the source-size gate is
|
|
210
|
+
// deliberately in neither FINAL_CORE_CHECKS nor REVIEW_DEPENDENT_CHECKS, so every surface asking
|
|
211
|
+
// "does this declaration verify the PROJECT?" through the review-dependent predicate alone read a
|
|
212
|
+
// matrix of nothing but that gate as project verification. Three surfaces asked it, each knowing a
|
|
213
|
+
// different half; this is the one home, so a future kit checker is added once.
|
|
214
|
+
export const isKitOwnedCheckerGate = (gate, projectDir) =>
|
|
215
|
+
isReviewDependentGate(gate, projectDir) || matchesSourceSizeGate(gate.cmd, projectDir);
|
|
216
|
+
|
|
205
217
|
// ── the pregate subset derivation (#66 / Decision 7 — ONE home for producer and factory) ─────
|
|
206
218
|
|
|
207
219
|
export const unknownPregateExcludeIds = (gates, exclude) => {
|
package/tools/gates-init.mjs
CHANGED
|
@@ -45,11 +45,12 @@
|
|
|
45
45
|
// tool path (spaces survive; executes from the project root).
|
|
46
46
|
//
|
|
47
47
|
// Write discipline: preview (dry-run) is the DEFAULT and writes NOTHING — a declined offer leaves
|
|
48
|
-
// the file byte-identical. `--apply`
|
|
48
|
+
// the file byte-identical. `--apply` writes EXACTLY the consented entries (`--only <id>`
|
|
49
49
|
// repeatable) through the shared atomic-write core (tools/atomic-write.mjs — exclusive-create
|
|
50
|
-
// tmp+rename, TOCTOU re-check, symlink STOPs):
|
|
51
|
-
// entry, refuses id collisions, refuses a malformed declaration (never writes over what it
|
|
52
|
-
// parse).
|
|
50
|
+
// tmp+rename, TOCTOU re-check, symlink STOPs): ADD-ONLY (it never modifies, removes or reorders an
|
|
51
|
+
// existing entry), refuses id collisions, refuses a malformed declaration (never writes over what it
|
|
52
|
+
// cannot parse). WHERE a consented entry lands is a PLACEMENT rule, not a blind append — see
|
|
53
|
+
// placeEntries below. Deployment-gated: docs/ai presence (lstat, no-follow) on EVERY run; the
|
|
53
54
|
// .workflow-version == lineage-head stamp gate on --apply only (the velocity/gate-hook precedent).
|
|
54
55
|
//
|
|
55
56
|
// Exit codes: 0 done / dry-run; 1 precondition STOP (no deployment, stamp, symlink, malformed
|
|
@@ -60,10 +61,20 @@ import { join, resolve, dirname } from 'node:path';
|
|
|
60
61
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
61
62
|
import { discoverGateCandidates, EXPECTED_WORKFLOW_VERSION } from './velocity-profile.mjs';
|
|
62
63
|
import { GATES_REL, validateDeclaration } from './run-gates.mjs';
|
|
63
|
-
import { coverageDeclarationDefects,
|
|
64
|
+
import { canonicalCheckerGates, coverageDeclarationDefects, isKitOwnedCheckerGate } from './gates-declaration.mjs';
|
|
64
65
|
import { COVERAGE_PRODUCER_BODY, matchesCoverageProducer } from './coverage-producer.mjs';
|
|
65
66
|
import { loadConfig } from './orchestration-config.mjs';
|
|
66
67
|
import { assertDocsAiDeployment, writeDocsAiFileAtomic, lstatNoFollow } from './atomic-write.mjs';
|
|
68
|
+
// The source-size practice, through its PURE READ core only (D-18): this module asks whether the
|
|
69
|
+
// practice is minted, never how to mint it — the checker's writer half stays outside the read graph.
|
|
70
|
+
import {
|
|
71
|
+
INITIAL_ADOPTION_REASON,
|
|
72
|
+
SOURCE_SIZE_GATE_ID,
|
|
73
|
+
SOURCE_SIZE_TOOL_PATH,
|
|
74
|
+
escapeForLine,
|
|
75
|
+
isLineUnsafe,
|
|
76
|
+
loadSourceSizeConfig,
|
|
77
|
+
} from './source-size-core.mjs';
|
|
67
78
|
|
|
68
79
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
69
80
|
const KIT_ROOT = resolve(HERE, '..');
|
|
@@ -95,9 +106,11 @@ export const TRUST_CHAIN_DISCLOSURE =
|
|
|
95
106
|
const USAGE = `usage: gates-init [--dry-run | --apply] [--only <id>]... [--cwd <dir>] [--help]
|
|
96
107
|
|
|
97
108
|
The consented FILL preview for the project's own docs/ai/gates.json (D9). Default is --dry-run:
|
|
98
|
-
prints the derived { id, title, cmd } entries and writes NOTHING. --apply
|
|
99
|
-
consented entries (--only <id> selects a subset;
|
|
100
|
-
|
|
109
|
+
prints the derived { id, title, cmd } entries and writes NOTHING. --apply WRITES exactly the
|
|
110
|
+
consented entries (--only <id> selects a subset; add-only — existing entries are never modified,
|
|
111
|
+
removed or reordered, an id collision is refused). A consented entry lands at the END, except that
|
|
112
|
+
a non-checker entry goes BEFORE a declaration's trailing canonical coverage checker, which must
|
|
113
|
+
stay last. The offer is CLOSED-WORLD: only a
|
|
101
114
|
terminating-class script name (test / lint / type-check / build — never release/publish/deploy,
|
|
102
115
|
never watch/serve, never a write-mode variant) whose BODY is a member of the literal runner
|
|
103
116
|
allowlist is offered, as the hook-free \`COREPACK_ENABLE_NETWORK=0 <pm> exec -- <body>\` form for
|
|
@@ -410,6 +423,77 @@ export const flowCheckCandidate = (cwd, deps = {}) => {
|
|
|
410
423
|
}
|
|
411
424
|
};
|
|
412
425
|
|
|
426
|
+
// The conditional SOURCE-SIZE candidate — keyed on the practice's own declaration rather than on a
|
|
427
|
+
// recipe: the gate is offered ONLY over a MINTED docs/ai/source-size.json, because on any other
|
|
428
|
+
// state the checker REFUSES by design (an absent config has no declared scope; an authored or
|
|
429
|
+
// half-written one records nothing yet), and a declared gate that refuses is a matrix this preview
|
|
430
|
+
// would have reddened on the user's behalf. The two states are told apart out loud: a project that
|
|
431
|
+
// never declared the practice hears nothing (there is no offer to make), while one that authored it
|
|
432
|
+
// and stopped short hears WHY the gate is not offered yet — the mint is the step it is missing.
|
|
433
|
+
// A path is unrenderable when EITHER guard fires, and both are asked ONCE, before anything about
|
|
434
|
+
// this candidate is printed. They are the same question wearing two coats: double-quoting cannot
|
|
435
|
+
// survive a shell-active byte, and a line-unsafe byte cannot be printed at all — U+2028 quotes
|
|
436
|
+
// perfectly and still breaks the line, so a surface screening only shell metacharacters lets it walk
|
|
437
|
+
// into a declared gate cmd while reading as fully guarded.
|
|
438
|
+
const unrenderableToolPath = (text) => isLineUnsafe(text) || DQ_UNSAFE_PATH_PATTERN.test(text);
|
|
439
|
+
|
|
440
|
+
export const sourceSizeCandidate = (cwd, deps = {}) => {
|
|
441
|
+
const toolPath = deps.sourceSizeTool ?? SOURCE_SIZE_TOOL_PATH;
|
|
442
|
+
try {
|
|
443
|
+
const { state } = loadSourceSizeConfig(resolve(cwd), deps);
|
|
444
|
+
if (state === 'absent') return { candidate: null, note: null };
|
|
445
|
+
// Asked BEFORE the state branches: every branch below prints this path, so a check that only one
|
|
446
|
+
// of them performs is a guard the other silently lacks.
|
|
447
|
+
if (unrenderableToolPath(toolPath)) {
|
|
448
|
+
// The withheld path is NAMED, so it crosses the line-safety boundary before it is rendered —
|
|
449
|
+
// escaped rather than dropped, because two different paths must never print as the same one.
|
|
450
|
+
// The advice still follows the STATE: over an unminted record a hand-declared gate is red on
|
|
451
|
+
// every run, so pointing there would hand the reader their next failure as the way out.
|
|
452
|
+
const lane = state === 'minted'
|
|
453
|
+
? 'declare the gate by hand'
|
|
454
|
+
: `mint the record first — run source-size-check.mjs --adopt --reason "${INITIAL_ADOPTION_REASON}" with the working directory set to this project, then declare the gate by hand`;
|
|
455
|
+
return {
|
|
456
|
+
candidate: null,
|
|
457
|
+
note:
|
|
458
|
+
`the source-size candidate was withheld: the resolved kit path cannot be rendered into a ` +
|
|
459
|
+
`command — it carries bytes that do not survive double-quoting, or a byte that cannot appear ` +
|
|
460
|
+
`in a printed line at all (${escapeForLine(toolPath)}) — ${lane}`,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
if (state !== 'minted') {
|
|
464
|
+
// The named recovery is the reader's next keystroke, so it is a command they can actually run:
|
|
465
|
+
// the resolved absolute path and an explicit --cwd, carrying the reason a FIRST mint requires
|
|
466
|
+
// (recording a value for the first time is a raise, and the bare verb would refuse). The
|
|
467
|
+
// PROJECT path is screened by the same predicate — a rendered command naming a path the shell
|
|
468
|
+
// would read differently could run somewhere other than the project it names.
|
|
469
|
+
const project = resolve(cwd);
|
|
470
|
+
const recovery = unrenderableToolPath(project)
|
|
471
|
+
? `run source-size-check.mjs --adopt --reason "${INITIAL_ADOPTION_REASON}" yourself, with the working directory set to this project (no command is printed: this project's path cannot be rendered into one)`
|
|
472
|
+
: `run node "${toolPath}" --adopt --reason "${INITIAL_ADOPTION_REASON}" --cwd "${project}"`;
|
|
473
|
+
return {
|
|
474
|
+
candidate: null,
|
|
475
|
+
note:
|
|
476
|
+
`the source-size candidate was withheld: the declared practice is not yet MINTED (${state}) — ` +
|
|
477
|
+
`the checker refuses until it records this tree, so declaring the gate now would red the matrix; ` +
|
|
478
|
+
`${recovery}, then re-run this preview`,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
candidate: {
|
|
483
|
+
id: SOURCE_SIZE_GATE_ID,
|
|
484
|
+
title: 'Source files within the declared size caps (the recorded ratchet)',
|
|
485
|
+
cmd: `node "${toolPath}" --check`,
|
|
486
|
+
},
|
|
487
|
+
note: null,
|
|
488
|
+
};
|
|
489
|
+
} catch (err) {
|
|
490
|
+
return {
|
|
491
|
+
candidate: null,
|
|
492
|
+
note: `the source-size practice config is unreadable (${err.message}) — the source-size candidate was not evaluated`,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
413
497
|
// Every --only id must name an OFFERED entry — enforced in BOTH paths (dry-run and apply), before
|
|
414
498
|
// any empty-offer shortcut, so a typo is a loud usage error, never a silent filter or a silent
|
|
415
499
|
// "nothing to offer" success.
|
|
@@ -424,15 +508,16 @@ const assertOnlyIdsOffered = (offer, onlyIds = []) => {
|
|
|
424
508
|
}
|
|
425
509
|
};
|
|
426
510
|
|
|
427
|
-
// The full offer: script entries + the conditional review-state / flow-check /
|
|
428
|
-
// candidates (coverage-check LAST — the `run-gates --final` declaration-shape rule
|
|
429
|
-
// checker as the last declared gate, so a whole-offer apply is final-ready by
|
|
430
|
-
// pair keys on plan-execution.review reviewed/council OR a flow block (the P21
|
|
431
|
-
// itself appears only under a flow block.
|
|
511
|
+
// The full offer: script entries + the conditional review-state / flow-check / source-size /
|
|
512
|
+
// coverage-check candidates (coverage-check LAST — the `run-gates --final` declaration-shape rule
|
|
513
|
+
// requires the checker as the last declared gate, so a whole-offer apply is final-ready by
|
|
514
|
+
// construction). The pair keys on plan-execution.review reviewed/council OR a flow block (the P21
|
|
515
|
+
// trio); flow-check itself appears only under a flow block; source-size only over a minted practice.
|
|
432
516
|
export const buildOffer = (cwd, deps = {}) => {
|
|
433
517
|
const scripts = deriveScripts(cwd, deps);
|
|
434
518
|
const rs = reviewStateCandidate(cwd, deps);
|
|
435
519
|
const fc = flowCheckCandidate(cwd, deps);
|
|
520
|
+
const ss = sourceSizeCandidate(cwd, deps);
|
|
436
521
|
const cc = coverageCheckCandidate(cwd, deps);
|
|
437
522
|
// Decision 3 — the checker is never offered DEAD. The producer may come from this offer or from
|
|
438
523
|
// a declaration the user already wrote by hand, so the rule reads the merged picture; a
|
|
@@ -462,7 +547,7 @@ export const buildOffer = (cwd, deps = {}) => {
|
|
|
462
547
|
// readable, no gate → the claim and the advice both hold;
|
|
463
548
|
// readable, has gate → a green matrix proves plenty and the user already declared their own.
|
|
464
549
|
const declarationState =
|
|
465
|
-
existing.unreadable !== null ? 'unreadable' : existing.gates.some((gate) => !
|
|
550
|
+
existing.unreadable !== null ? 'unreadable' : existing.gates.some((gate) => !isKitOwnedCheckerGate(gate, cwd)) ? 'has-gate' : 'no-gate';
|
|
466
551
|
const noVerificationNote =
|
|
467
552
|
scripts.entries.length > 0
|
|
468
553
|
? null
|
|
@@ -471,10 +556,10 @@ export const buildOffer = (cwd, deps = {}) => {
|
|
|
471
556
|
? `, and none is declared either, so a green matrix would prove only that the kit's own checkers ran; declare your own in ${GATES_REL}`
|
|
472
557
|
: ''
|
|
473
558
|
}`;
|
|
474
|
-
const candidates = [rs.candidate, fc.candidate, withholdCoverage ? null : cc.candidate].filter(Boolean);
|
|
559
|
+
const candidates = [rs.candidate, fc.candidate, ss.candidate, withholdCoverage ? null : cc.candidate].filter(Boolean);
|
|
475
560
|
return {
|
|
476
561
|
entries: [...scripts.entries, ...candidates],
|
|
477
|
-
notes: [...scripts.notes, noVerificationNote, unreadableNote, rs.note, fc.note, coverageNote].filter(Boolean),
|
|
562
|
+
notes: [...scripts.notes, noVerificationNote, unreadableNote, rs.note, fc.note, ss.note, coverageNote].filter(Boolean),
|
|
478
563
|
};
|
|
479
564
|
};
|
|
480
565
|
|
|
@@ -510,7 +595,7 @@ export const formatPreview = (offer, applyInvocation = null, { explicitOnly = fa
|
|
|
510
595
|
return lines.join('\n');
|
|
511
596
|
};
|
|
512
597
|
|
|
513
|
-
// ── the existing declaration (
|
|
598
|
+
// ── the existing declaration (the base every placement is computed against) ────────────
|
|
514
599
|
const loadExistingDeclaration = (cwd, deps = {}) => {
|
|
515
600
|
const read = deps.readFile ?? readFileSync;
|
|
516
601
|
const lstat = deps.lstat ?? lstatSync;
|
|
@@ -572,7 +657,30 @@ const readStampValue = (cwd, deps = {}) => {
|
|
|
572
657
|
}
|
|
573
658
|
};
|
|
574
659
|
|
|
575
|
-
// ──
|
|
660
|
+
// ── the PLACEMENT rule (D-8) ───────────────────────────────────────────────────────────
|
|
661
|
+
// WHERE a consented entry lands. A blind append made the fill unusable on exactly the declarations
|
|
662
|
+
// it should serve best: the canonical coverage checker must be the LAST gate, so appending any
|
|
663
|
+
// other entry after it produced a declaration the written-declaration validator correctly reds —
|
|
664
|
+
// and the only way to consent to a new gate on a final-capable declaration was to hand-edit.
|
|
665
|
+
//
|
|
666
|
+
// The rule is one sentence: a non-checker entry goes BEFORE a trailing canonical checker, everything
|
|
667
|
+
// else goes at the end. It is ADD-ONLY still — existing entries keep their relative order, none is
|
|
668
|
+
// modified or removed; only the insertion point moves. A consented CHECKER stays after the trailing
|
|
669
|
+
// one on purpose: two canonical checkers is a duplicate the validator must refuse by name, and
|
|
670
|
+
// hiding that shape behind a clever placement would refuse it for the wrong reason.
|
|
671
|
+
export const placeEntries = (existingGates, selected, projectDir) => {
|
|
672
|
+
const last = existingGates[existingGates.length - 1];
|
|
673
|
+
const isChecker = (gate) => canonicalCheckerGates([gate], projectDir).length === 1;
|
|
674
|
+
if (last === undefined || !isChecker(last)) return [...existingGates, ...selected];
|
|
675
|
+
return [
|
|
676
|
+
...existingGates.slice(0, -1),
|
|
677
|
+
...selected.filter((entry) => !isChecker(entry)),
|
|
678
|
+
last,
|
|
679
|
+
...selected.filter(isChecker),
|
|
680
|
+
];
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// ── apply (write exactly the consented entries, each at its placement) ─────────────────
|
|
576
684
|
export const applyFill = ({ cwd, onlyIds = [] }, deps = {}) => {
|
|
577
685
|
assertDocsAiDeployment(cwd, deps, { stop, noun: 'a gate declaration', rel: GATES_REL });
|
|
578
686
|
const stampValue = readStampValue(cwd, deps);
|
|
@@ -593,14 +701,14 @@ export const applyFill = ({ cwd, onlyIds = [] }, deps = {}) => {
|
|
|
593
701
|
const collisions = selected.filter((e) => existingIds.has(e.id)).map((e) => e.id);
|
|
594
702
|
if (collisions.length) {
|
|
595
703
|
throw stop(
|
|
596
|
-
`id collision — already declared in ${GATES_REL}: ${collisions.join(', ')} (
|
|
704
|
+
`id collision — already declared in ${GATES_REL}: ${collisions.join(', ')} (add-only: the ` +
|
|
597
705
|
`fill never modifies or removes an existing entry; pick the others with --only, or edit by hand)`,
|
|
598
706
|
);
|
|
599
707
|
}
|
|
600
708
|
|
|
601
709
|
const merged = {
|
|
602
710
|
_README: existing.outcome === 'loaded' && existing.readme !== undefined ? existing.readme : templateReadme(deps),
|
|
603
|
-
gates:
|
|
711
|
+
gates: placeEntries(existingGates, selected, cwd),
|
|
604
712
|
};
|
|
605
713
|
validateDeclaration(merged); // every written declaration passes the runner's validator, always
|
|
606
714
|
// Decision 4 — the coverage invariant is enforced on the declaration that GETS WRITTEN, not on
|
|
@@ -610,7 +718,11 @@ export const applyFill = ({ cwd, onlyIds = [] }, deps = {}) => {
|
|
|
610
718
|
if (defects.length) throw stop(`${defects[0].message} — nothing was written`);
|
|
611
719
|
const body = `${JSON.stringify(merged, null, 2)}\n`;
|
|
612
720
|
const { writtenPath } = writeDocsAiFileAtomic(cwd, GATES_REL, body, deps, { stop, noun: 'a gate declaration' });
|
|
613
|
-
|
|
721
|
+
const placed = selected.map((e) => e.id);
|
|
722
|
+
// `appended` is the DEPRECATED alias of `placed`, carrying the same array: this result is a public
|
|
723
|
+
// tools/ payload, and dropping a field an external consumer reads would make a placement rule a
|
|
724
|
+
// BREAKING change. The name is the only thing that was ever wrong — the value never was.
|
|
725
|
+
return { outcome: 'written', writtenPath, placed, appended: placed, notes: offer.notes };
|
|
614
726
|
};
|
|
615
727
|
|
|
616
728
|
// ── CLI ────────────────────────────────────────────────────────────────────────────────
|
|
@@ -670,7 +782,7 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
670
782
|
for (const note of result.notes) log(` note: ${note}`); // the user learns WHY (same note as the preview)
|
|
671
783
|
return EXIT_OK;
|
|
672
784
|
}
|
|
673
|
-
log(`[agent-workflow-kit]
|
|
785
|
+
log(`[agent-workflow-kit] declared ${result.placed.length} consented gate(s) in ${GATES_REL}: ${result.placed.join(', ')}`);
|
|
674
786
|
for (const note of result.notes) log(` note: ${note}`); // a mixed offer never silently omits what was screened
|
|
675
787
|
log(`[agent-workflow-kit] ${TRUST_CHAIN_DISCLOSURE}`);
|
|
676
788
|
return EXIT_OK;
|
package/tools/procedures.mjs
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// steps LIVE from the installed agent-workflow-engine (references/procedures.md — AD-016 live read, no
|
|
6
6
|
// bundled mirror), reads the per-project, hand-edited config (docs/ai/orchestration.json), runs the
|
|
7
7
|
// read-only backend detector, and prints the activity's steps VERBATIM + the resolved effective recipe
|
|
8
|
-
// per slot (default = Reviewed-when-a-backend-is-ready, Council on request, slot-aware incl. Delegated)
|
|
8
|
+
// per slot (default = Reviewed-when-a-backend-is-ready, Council on request, slot-aware incl. Delegated),
|
|
9
|
+
// plus the project's DECLARED source-size practice when it declares one (D-17 U1).
|
|
9
10
|
//
|
|
10
11
|
// Invariants (mirror recipes.mjs): pure-where-possible, READ-ONLY (never writes, never commits, never
|
|
11
12
|
// runs a subscription CLI). The deterministic resolution lives in the kit (resolveActivityRecipe), not
|
|
@@ -44,6 +45,10 @@ import { AUTONOMY_REL, loadAutonomy, resolveAutonomy, isSparseSeedConfig } from
|
|
|
44
45
|
// acyclic — pinned by test/read-graph-purity.test.mjs (FLOW-READ-GRAPH-PURITY).
|
|
45
46
|
import { resolveFlowStorePath, readFlowStore } from './flow-store-read.mjs';
|
|
46
47
|
import { CHAIN_KIND } from './flow-record.mjs';
|
|
48
|
+
// The declared source-size practice (D-17 U1), read through the practice's PURE READ core — never
|
|
49
|
+
// source-size-check.mjs, which owns the writer half: this advisor is a read root of
|
|
50
|
+
// test/read-graph-purity.test.mjs, and the core exists so a surface can ask without reaching a writer.
|
|
51
|
+
import { SOURCE_SIZE_CONFIG_REL, SOURCE_SIZE_WHY, loadSourceSizeConfig, practiceFacts } from './source-size-core.mjs';
|
|
47
52
|
export { CONFIG_REL };
|
|
48
53
|
|
|
49
54
|
// ── argument + override parsing (usage errors → exit 2) ─────────────────────────────
|
|
@@ -361,6 +366,49 @@ const flowHalvesAdvice = (flow, probe) => {
|
|
|
361
366
|
];
|
|
362
367
|
};
|
|
363
368
|
|
|
369
|
+
// ── the declared source-size practice (D-17 U1) ────────────────────────────────────
|
|
370
|
+
// A practice the agent meets only when a gate refuses is a practice learned too late: the caps, their
|
|
371
|
+
// reason and the plan-time rung ride EVERY named-activity render, so the layout is cut to them while
|
|
372
|
+
// the plan is being written. Composed from the project's live declaration, never from constants here.
|
|
373
|
+
// Each config state speaks as itself: ABSENT renders NOTHING (a project that declares no practice must
|
|
374
|
+
// not be handed invented limits); AUTHORED and INCOMPLETE render the declared caps plus the honest
|
|
375
|
+
// "nothing is recorded yet" line — both are pre-mint states, and treating INCOMPLETE as MINTED would
|
|
376
|
+
// report a half record as the whole tree's debt; MINTED renders the recorded counts too.
|
|
377
|
+
// A config that cannot be read renders ONE loud line carrying the reader's own message and the render
|
|
378
|
+
// still completes: the exit code for a broken source-size config belongs to the practice's own
|
|
379
|
+
// checker (exit 2 there, and its declared gate reds the matrix on it), while THIS tool's exit
|
|
380
|
+
// contract is about its own config and the engine.
|
|
381
|
+
|
|
382
|
+
export const DECLARED_PRACTICE_HEADER = `Declared source-size practice (${SOURCE_SIZE_CONFIG_REL}) — known BEFORE the code is written:`;
|
|
383
|
+
|
|
384
|
+
const declaredPracticeAdvice = (cwd, readFile, lstat) => {
|
|
385
|
+
let declaration;
|
|
386
|
+
try {
|
|
387
|
+
declaration = loadSourceSizeConfig(cwd, { readFile, lstat });
|
|
388
|
+
} catch (err) {
|
|
389
|
+
return [`Declared source-size practice: UNREADABLE — ${(err && err.message) || err} — fix ${SOURCE_SIZE_CONFIG_REL} by hand; a declared practice is never guessed around.`];
|
|
390
|
+
}
|
|
391
|
+
if (declaration.state === 'absent') return [];
|
|
392
|
+
const facts = practiceFacts(declaration.config);
|
|
393
|
+
// The two pre-mint states share a LANE (the ratchet holds nothing, the mint step is next) but not a
|
|
394
|
+
// FACT: an incomplete file carries half the machine record, so "no size is recorded" would be a
|
|
395
|
+
// plain untruth about it — it names the missing half instead. Neither ever prints minted counts.
|
|
396
|
+
// The mint step is named, not rendered: on a project path that does not survive double-quoting the
|
|
397
|
+
// checker deliberately withholds a paste-ready command, and this advisor never re-decides that.
|
|
398
|
+
const unmintedRecord = declaration.state === 'incomplete'
|
|
399
|
+
? ` recorded: PARTIAL — the machine record is half-written (missing ${declaration.missingMachineKeys.map((key) => `"${key}"`).join(', ')}), so the ratchet holds nothing yet; run \`source-size-check.mjs --check\` for the mint step.`
|
|
400
|
+
: ' recorded: NOTHING YET — the caps are declared but no size is recorded, so the ratchet holds nothing; run `source-size-check.mjs --check` for the mint step.';
|
|
401
|
+
return [
|
|
402
|
+
DECLARED_PRACTICE_HEADER,
|
|
403
|
+
` caps: ${facts.maxLines} lines · ${facts.maxLineBytes} bytes per line, over ${facts.roots} declared root(s).`,
|
|
404
|
+
declaration.state === 'minted'
|
|
405
|
+
? ` recorded: ${facts.recordedFiles} file(s) carry a recorded size (debt, not permission) · aggregate ${facts.aggregateLines} line(s), EXACT — growth takes a reasoned bump, never free headroom.`
|
|
406
|
+
: unmintedRecord,
|
|
407
|
+
` why: ${SOURCE_SIZE_WHY}`,
|
|
408
|
+
' at plan time: every Step that CREATES a file names the file and its single responsibility, and the planned layout fits these caps — the gate is the backstop, never the teacher.',
|
|
409
|
+
];
|
|
410
|
+
};
|
|
411
|
+
|
|
364
412
|
// The verbatim per-backend DRIVING CONTRACT block (M-contract): the exact invocation descriptor(s),
|
|
365
413
|
// the closed flag set, the grounding note, the round-2/continue delta, and the guarded passthrough
|
|
366
414
|
// tiers — every descriptor printed VERBATIM from the registry mirror of the bridge manifest
|
|
@@ -397,7 +445,7 @@ const contractLines = ({ cmd, contract, settings }) => {
|
|
|
397
445
|
return lines;
|
|
398
446
|
};
|
|
399
447
|
|
|
400
|
-
const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flowHalves }) => {
|
|
448
|
+
const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flowHalves, declaredPractice }) => {
|
|
401
449
|
const lines = [
|
|
402
450
|
section,
|
|
403
451
|
'',
|
|
@@ -417,6 +465,7 @@ const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flow
|
|
|
417
465
|
const advice = reviewLoopAdvice(slots, activity);
|
|
418
466
|
if (advice.length) lines.push('', ...advice);
|
|
419
467
|
lines.push('', ...costLanesAdvice());
|
|
468
|
+
if (declaredPractice.length) lines.push('', ...declaredPractice);
|
|
420
469
|
if (warnings.length) {
|
|
421
470
|
lines.push('', 'warnings:');
|
|
422
471
|
for (const w of warnings) lines.push(` ⚠ ${w}`);
|
|
@@ -424,7 +473,7 @@ const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flow
|
|
|
424
473
|
return lines.join('\n');
|
|
425
474
|
};
|
|
426
475
|
|
|
427
|
-
const buildJson = ({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves }) => ({
|
|
476
|
+
const buildJson = ({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, declaredPractice }) => ({
|
|
428
477
|
activity,
|
|
429
478
|
section,
|
|
430
479
|
slots: Object.fromEntries(
|
|
@@ -439,6 +488,9 @@ const buildJson = ({ activity, section, slots, configSource, warnings, plans, au
|
|
|
439
488
|
costLanes: costLanesAdvice(),
|
|
440
489
|
// ADDITIVE (AD-044 Plan 4): the per-activity autonomy block, structured (empty when unresolvable).
|
|
441
490
|
autonomy: autonomyAdvice(activity, autonomy),
|
|
491
|
+
// ADDITIVE (D-17 U1): the SAME composed lines the human render prints — one array, two renders, so
|
|
492
|
+
// a scripted reader and a human can never be told different things about the declared practice.
|
|
493
|
+
declaredPractice,
|
|
442
494
|
// CONDITIONAL (flow P8): the armed-halves block rides ONLY a flow-carrying config — the unarmed
|
|
443
495
|
// JSON key set stays byte-exact (unarmed neutrality outranks the additive-key precedent).
|
|
444
496
|
...(flowHalves == null ? {} : { flowHalves }),
|
|
@@ -461,6 +513,12 @@ ${CONFIG_REL} + the read-only backend detector, and prints both. A per-run
|
|
|
461
513
|
--override <slot>=<recipe> (repeatable) overrides the configured/default recipe for that slot.
|
|
462
514
|
Read-only: never writes, never commits, never runs a subscription CLI.
|
|
463
515
|
|
|
516
|
+
Also prints the project's DECLARED source-size practice (${SOURCE_SIZE_CONFIG_REL}) when it declares
|
|
517
|
+
one — the caps, what is recorded, why the practice exists, and the plan-time rung — as the
|
|
518
|
+
declaredPractice block (--json: the same lines under "declaredPractice"). A project with no such
|
|
519
|
+
file renders nothing; a file that cannot be read renders ONE loud UNREADABLE line and still exits 0
|
|
520
|
+
(the practice's own checker owns the exit code for its config).
|
|
521
|
+
|
|
464
522
|
Exit codes: 0 success (an unsatisfiable override degrades loudly, still 0);
|
|
465
523
|
2 usage (unknown activity / bad --override); 1 config or engine error
|
|
466
524
|
(incl. a malformed ${AUTONOMY_REL} — the advisory still renders, the exit flips).`;
|
|
@@ -515,9 +573,10 @@ export const main = (argv, ctx = {}) => {
|
|
|
515
573
|
// unarmed project keeps byte-identical output (human AND JSON) and never pays the store probe.
|
|
516
574
|
const flowProbe = ctx.flowProbe ?? defaultFlowProbe;
|
|
517
575
|
const flowHalves = config?.flow == null ? null : flowHalvesAdvice(config.flow, flowProbe(cwd));
|
|
576
|
+
const declaredPractice = declaredPracticeAdvice(cwd, readFile, lstat);
|
|
518
577
|
const stdout = json
|
|
519
|
-
? JSON.stringify(buildJson({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves }), null, 2)
|
|
520
|
-
: formatHuman({ activity, section, slots, warnings, plans, autonomy, flowHalves });
|
|
578
|
+
? JSON.stringify(buildJson({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, declaredPractice }), null, 2)
|
|
579
|
+
: formatHuman({ activity, section, slots, warnings, plans, autonomy, flowHalves, declaredPractice });
|
|
521
580
|
if (autonomy?.error) {
|
|
522
581
|
return { code: 1, stdout, stderr: `procedures: malformed ${AUTONOMY_REL} — ${autonomy.error}` };
|
|
523
582
|
}
|
|
@@ -55,7 +55,13 @@ import { surveyFamily, surveyGateHook, surveyAdrLayoutStrict } from './family-re
|
|
|
55
55
|
import { probeSandboxMasks, needsMasksApply } from './sandbox-masks.mjs';
|
|
56
56
|
import { shellQuoteArg } from './review-state.mjs';
|
|
57
57
|
import { isFinalCapableDeclaration } from './run-gates.mjs';
|
|
58
|
-
import { loadDeclaration, canonicalCheckerGates, coverageProducerPrecedes,
|
|
58
|
+
import { loadDeclaration, canonicalCheckerGates, coverageProducerPrecedes, isKitOwnedCheckerGate, GATES_REL } from './gates-declaration.mjs';
|
|
59
|
+
import { matchesCoverageProducer } from './coverage-producer.mjs';
|
|
60
|
+
// Read-only surfaces of the fill (buildOffer) and of the source-size practice (its pure core). The
|
|
61
|
+
// fill's own WRITER is never called from here — the advisor renders its consent-gated command, it
|
|
62
|
+
// does not run it.
|
|
63
|
+
import { buildOffer } from './gates-init.mjs';
|
|
64
|
+
import { INITIAL_ADOPTION_REASON, loadSourceSizeConfig, matchesSourceSizeGate } from './source-size-core.mjs';
|
|
59
65
|
// The declared-path resolution + segment containment this item's convergence lane shares with the
|
|
60
66
|
// autonomy render's allowWrite degrade — ONE leaf, so the two answers cannot drift.
|
|
61
67
|
import { resolveDeclaredDir, dirCovers, isResolvableDeclaredEntry } from './declared-paths.mjs';
|
|
@@ -106,6 +112,10 @@ export const SEVERITIES = Object.freeze({
|
|
|
106
112
|
'gates-declaration': SEVERITY_OPTIONAL,
|
|
107
113
|
'gates-inert': SEVERITY_ATTENTION,
|
|
108
114
|
'gates-inert.no-verification': SEVERITY_ATTENTION,
|
|
115
|
+
'source-size': SEVERITY_OPTIONAL,
|
|
116
|
+
// The declared-but-unminted arm reports a CONFIGURED declaration that is broken — a gate certain
|
|
117
|
+
// to refuse on every run — while the base arm stays an offer to enable something unconfigured.
|
|
118
|
+
'source-size.unminted': SEVERITY_ATTENTION,
|
|
109
119
|
'gate-hook': SEVERITY_OPTIONAL,
|
|
110
120
|
'commit-guard': SEVERITY_OPTIONAL,
|
|
111
121
|
'read-lane': SEVERITY_OPTIONAL,
|
|
@@ -166,6 +176,8 @@ export const WHATS = Object.freeze({
|
|
|
166
176
|
'gates-declaration': 'no declared gate matrix (docs/ai/gates.json absent or empty) — gates prompt one by one; the apply PREVIEWS its --apply line, writes nothing',
|
|
167
177
|
'gates-inert': 'the declared coverage checker ({id}) has no producer before it — it certifies nothing this run, or reads a stale lcov',
|
|
168
178
|
'gates-inert.no-verification': "all {n} declared gate(s) are the kit's own checkers — the matrix runs no project-verification command",
|
|
179
|
+
'source-size': 'no source-size gate — module size drifts unmeasured, and an over-cap file is invisible instead of recorded debt',
|
|
180
|
+
'source-size.unminted': 'the source-size gate is declared but its record ({state}) is not minted — the checker refuses, so this gate reds every run',
|
|
169
181
|
'gate-hook': '{n} declared gate(s) prompt per run — the gate-approval hook is not wired',
|
|
170
182
|
'commit-guard': 'the gate matrix is final-run-capable but no commit-guard arms the pre-commit hook — a commit needs no green receipt yet',
|
|
171
183
|
'read-lane': 'the gate hook is wired but the read-only compound lane is off — pipes/chains of seeded reads still prompt one by one',
|
|
@@ -223,6 +235,7 @@ export const BENEFITS = Object.freeze({
|
|
|
223
235
|
'review-recipe': 'recipe coverage — the review AND execution recipes you configured actually run instead of silently degrading',
|
|
224
236
|
'gates-declaration': 'velocity — your project’s gates run as ONE declared batch with a PASS/FAIL table',
|
|
225
237
|
'gates-inert': 'honest gates — the declared matrix verifies your project instead of reporting green over a check that ran nothing',
|
|
238
|
+
'source-size': 'maintainability — a module you can hold whole stays reviewable, and size drift becomes recorded, reasoned debt',
|
|
226
239
|
'gate-hook': 'velocity — your own declared gate commands auto-approve byte-exactly (opt-in PreToolUse hook)',
|
|
227
240
|
'commit-guard': 'integrity — commits require the ONE green --final receipt at the exact staged fingerprint (consented pre-commit arm)',
|
|
228
241
|
'read-lane': 'velocity — pipes/chains of your seeded read-only commands auto-approve instead of prompting (opt-in, conservatively classified)',
|
|
@@ -263,6 +276,10 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
|
|
|
263
276
|
// converges the moment any gate exists, so it can never observe this state. The advisor key names
|
|
264
277
|
// the state it reports (an inert declaration), the capability names what the user gains.
|
|
265
278
|
{ id: 'gates-verification', mode: 'gates', advisorKey: 'gates-inert' },
|
|
279
|
+
// The source-size practice is accepted AS a gate — it needs no mode of its own, and its capability
|
|
280
|
+
// is therefore declared where its declaration lives. It is the DISCOVERY lane for the practice on
|
|
281
|
+
// every deployment, new and existing alike: nothing else tells a project the practice exists.
|
|
282
|
+
{ id: 'source-size', mode: 'gates', advisorKey: 'source-size' },
|
|
266
283
|
{ id: 'gate-hook', mode: 'hook', advisorKey: 'gate-hook' },
|
|
267
284
|
{ id: 'read-lane', mode: 'hook', advisorKey: 'read-lane' },
|
|
268
285
|
{ id: 'commit-guard', mode: 'commit-guard', advisorKey: 'commit-guard' },
|
|
@@ -432,12 +449,23 @@ const probeGates = ({ root, deps, add, skip }) => {
|
|
|
432
449
|
//
|
|
433
450
|
// Cause A (a canonical coverage checker with no producer anywhere in the declaration) is checked
|
|
434
451
|
// FIRST and reported alone: its remedy — declaring the producer — also resolves cause B, because a
|
|
435
|
-
// producer gate is not a kit checker. Its apply
|
|
436
|
-
//
|
|
437
|
-
// the
|
|
452
|
+
// producer gate is not a kit checker. Its apply follows what the FILL can actually do, which the
|
|
453
|
+
// D-8 placement rule changed: when the checker is the declaration's LAST gate and the project's own
|
|
454
|
+
// scripts yield an offerable producer, the fill now PLACES that producer before the checker, so the
|
|
455
|
+
// remedy is the ordinary consent-gated preview. HAND-APPLY remains exactly where no offerable
|
|
456
|
+
// producer exists, or where the checker is not last — there the edit really is the maintainer's,
|
|
457
|
+
// because the fill never reorders entries it did not write.
|
|
438
458
|
//
|
|
439
459
|
// An ABSENT or EMPTY declaration belongs to the gates-declaration item; a malformed one throws out
|
|
440
460
|
// of the validated reader and becomes this probe's stated skip, never a guess.
|
|
461
|
+
// ONE rule for every fill preview this item renders: name only the entries the fill would ACCEPT.
|
|
462
|
+
// An id the declaration already carries is refused as a collision, so an unrestricted preview hands
|
|
463
|
+
// the reader a lane that cannot converge — and both arms of this item render over a declaration that
|
|
464
|
+
// already carries at least one gate the offer also proposes, so both need the rule. With nothing
|
|
465
|
+
// selectable the bare preview is still the honest render: its own notes say why.
|
|
466
|
+
const fillPreviewFor = (root, ids) =>
|
|
467
|
+
`node ${q(toolPath('gates-init.mjs'))} --cwd ${q(root)}${ids.map((id) => ` --only ${id}`).join('')}`;
|
|
468
|
+
|
|
441
469
|
export const probeGatesInert = ({ root, deps, add, skip }) => {
|
|
442
470
|
try {
|
|
443
471
|
const declaration = loadDeclaration(root, deps);
|
|
@@ -451,18 +479,54 @@ export const probeGatesInert = ({ root, deps, add, skip }) => {
|
|
|
451
479
|
// only --final refuses that shape — a plain run reports every gate PASS.
|
|
452
480
|
if (coverageProducerPrecedes(gates, gates.indexOf(checkers[0]))) return; // the pair is live
|
|
453
481
|
const id = truncatedTo(oneLineOf(checkers[0].id), templateBudget(WHATS['gates-inert']));
|
|
482
|
+
// The fill can only help when it would land the producer in the right place: the checker must
|
|
483
|
+
// be LAST (that is the one position the placement rule inserts before) and the offer must
|
|
484
|
+
// actually carry a producer the fill would ACCEPT. An offered id that is already declared is
|
|
485
|
+
// refused as a collision, so counting it here would render a preview that cannot fix what the
|
|
486
|
+
// item just reported.
|
|
487
|
+
const declaredIds = new Set(gates.map((gate) => gate.id));
|
|
488
|
+
const checkerIsLast = checkers[0] === gates[gates.length - 1];
|
|
489
|
+
// The PRODUCER entry is kept, not just its existence: the rendered preview must name it with
|
|
490
|
+
// --only. A whole-offer apply here collides by construction — the declaration already carries
|
|
491
|
+
// the checker, and the offer carries it too — so an unrestricted preview would hand the reader
|
|
492
|
+
// a lane that refuses instead of the one entry that resolves what the item just reported.
|
|
493
|
+
const producer = buildOffer(root, deps).entries
|
|
494
|
+
.find((entry) => !declaredIds.has(entry.id) && matchesCoverageProducer(entry.cmd));
|
|
495
|
+
// The two ways the fill cannot help are DIFFERENT situations and need different sentences: a
|
|
496
|
+
// checker that is not last blocks a placement even when a producer is offerable, and saying no
|
|
497
|
+
// producer exists there would be plainly false to a reader looking at their own scripts.
|
|
498
|
+
const blocked = checkerIsLast
|
|
499
|
+
? `no offerable producer exists here, and the fill never reorders entries it did not write`
|
|
500
|
+
: `${id} is not the LAST declared gate, so there is no trailing position to place a producer before, and the fill never reorders entries it did not write`;
|
|
454
501
|
add(
|
|
455
502
|
'gates-inert',
|
|
456
503
|
fillTemplate(WHATS['gates-inert'], { id }),
|
|
457
|
-
|
|
504
|
+
checkerIsLast && producer
|
|
505
|
+
? fillPreviewFor(root, [producer.id])
|
|
506
|
+
: `HAND-APPLY: declare or MOVE a suite gate carrying the coverage reporters BEFORE ${id} in ${GATES_REL} (references/modes/gates.md names the exact form), or drop ${id} — ${blocked}`,
|
|
458
507
|
);
|
|
459
508
|
return;
|
|
460
509
|
}
|
|
461
|
-
|
|
510
|
+
// The source-size checker is one of the kit's OWN checkers, and it is deliberately NOT
|
|
511
|
+
// review-dependent (it needs no receipt), so the review-dependent predicate alone cannot see it.
|
|
512
|
+
// Left out, a matrix of nothing but that gate reads as carrying project verification — and a
|
|
513
|
+
// project that just adopted the practice and declared nothing else would be told it is optimal.
|
|
514
|
+
// The source-size checker is one of the kit's OWN checkers, and it is deliberately NOT
|
|
515
|
+
// review-dependent (it needs no receipt), so the review-dependent predicate alone cannot see it.
|
|
516
|
+
// Left out, a matrix of nothing but that gate reads as carrying project verification — and a
|
|
517
|
+
// project that just adopted the practice and declared nothing else would be told it is optimal.
|
|
518
|
+
if (gates.every((gate) => isKitOwnedCheckerGate(gate, root))) {
|
|
519
|
+
const declaredIds = new Set(gates.map((gate) => gate.id));
|
|
520
|
+
// Only PROJECT-verification entries: a non-colliding entry is not enough, it has to be one
|
|
521
|
+
// that RESOLVES the item, and declaring one more kit checker converges nothing — the item
|
|
522
|
+
// would simply fire again on the next run.
|
|
523
|
+
const selectable = buildOffer(root, deps).entries
|
|
524
|
+
.filter((entry) => !declaredIds.has(entry.id) && !isKitOwnedCheckerGate(entry, root))
|
|
525
|
+
.map((entry) => entry.id);
|
|
462
526
|
add(
|
|
463
527
|
'gates-inert',
|
|
464
528
|
fillTemplate(WHATS['gates-inert.no-verification'], { n: gates.length }),
|
|
465
|
-
|
|
529
|
+
fillPreviewFor(root, selectable),
|
|
466
530
|
'gates-inert.no-verification',
|
|
467
531
|
);
|
|
468
532
|
}
|
|
@@ -471,6 +535,42 @@ export const probeGatesInert = ({ root, deps, add, skip }) => {
|
|
|
471
535
|
}
|
|
472
536
|
};
|
|
473
537
|
|
|
538
|
+
// The SOURCE-SIZE offer (baseline-practices Plan 1). This is the practice's ONE discovery lane: the
|
|
539
|
+
// checker ships with the kit and refuses until a project declares its own scope, so without this
|
|
540
|
+
// item a deployment would never learn the practice exists — the same OPT-IN-SHIPS-INVISIBLE failure
|
|
541
|
+
// the capability registry was built for. New and existing deployments meet it identically, because
|
|
542
|
+
// the advisor section is mandatory at every upgrade.
|
|
543
|
+
//
|
|
544
|
+
// The probe asks ONE question — does the declaration carry the canonical source-size gate? — through
|
|
545
|
+
// the practice's own matcher, so an id squatter (a gate called `source-size` running something else)
|
|
546
|
+
// never reads as adopted. It deliberately does NOT key on the config's state: a project with no
|
|
547
|
+
// config is exactly the project that needs to hear about the practice, and the apply's own refusal
|
|
548
|
+
// is what teaches the one manual step (authoring the scope). A missing declaration is not a skip —
|
|
549
|
+
// there is no source-size gate in it either.
|
|
550
|
+
const probeSourceSize = ({ root, deps, add, skip }) => {
|
|
551
|
+
try {
|
|
552
|
+
const declaration = loadDeclaration(root, deps);
|
|
553
|
+
const gates = declaration.outcome === 'loaded' ? declaration.gates : [];
|
|
554
|
+
const applyLine = `node ${q(toolPath('source-size-check.mjs'))} --adopt --reason "${INITIAL_ADOPTION_REASON}" --cwd ${q(root)}`;
|
|
555
|
+
if (gates.some((gate) => matchesSourceSizeGate(gate.cmd, root))) {
|
|
556
|
+
// A DECLARED gate is not the same fact as a working one: the checker refuses on every config
|
|
557
|
+
// state but MINTED, so a gate declared over an absent or half-written record reds the matrix
|
|
558
|
+
// on every run. Reading the declaration alone would report that deployment as adopted and say
|
|
559
|
+
// nothing about the one thing that is wrong with it.
|
|
560
|
+
const { state } = loadSourceSizeConfig(root, deps);
|
|
561
|
+
if (state === 'minted') return; // adopted and armed — converged
|
|
562
|
+
add('source-size', fillTemplate(WHATS['source-size.unminted'], { state }), applyLine, 'source-size.unminted');
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
// The reason string is PINNED, not composed: it is copied unchanged into every entry the first
|
|
566
|
+
// mint records, and a first mint records the whole tree — so "this is what the tree already
|
|
567
|
+
// carried when the practice arrived" is the one sentence that is true of all of them.
|
|
568
|
+
add('source-size', fillTemplate(WHATS['source-size'], {}), applyLine);
|
|
569
|
+
} catch (err) {
|
|
570
|
+
skip('source-size', err);
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
|
|
474
574
|
// The D10 consumer surface: once the declaration is FINAL-capable (the canonical core checks
|
|
475
575
|
// present, the checker LAST — the run-gates helper is the one home of that rule), offer the
|
|
476
576
|
// consented commit-guard install — for a MANAGED guardless pre-commit hook AND for an absent one
|
|
@@ -1054,6 +1154,7 @@ const PROBES = Object.freeze([
|
|
|
1054
1154
|
probeReviewRecipe,
|
|
1055
1155
|
probeGates,
|
|
1056
1156
|
probeGatesInert,
|
|
1157
|
+
probeSourceSize,
|
|
1057
1158
|
probeCommitGuard,
|
|
1058
1159
|
probeReadLane,
|
|
1059
1160
|
probeStateBlockHook,
|