@sabaiway/agent-workflow-kit 5.5.0 → 5.7.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 +122 -0
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/hooks/gate-approve.mjs +7 -1
- package/references/modes/doc-parity.md +1 -1
- package/references/modes/gates.md +20 -4
- package/references/modes/procedures.md +2 -0
- package/references/modes/recommendations.md +4 -1
- package/references/modes/review-state.md +1 -1
- package/references/modes/setup.md +18 -2
- package/references/modes/upgrade.md +38 -18
- package/references/modes/velocity.md +1 -0
- package/references/scripts/migrate-gates-branches.test.mjs +146 -1
- package/references/scripts/migrate-gates.mjs +295 -60
- package/references/scripts/migrate-gates.test.mjs +206 -14
- package/references/shared/deploy-tail.md +1 -1
- package/references/templates/gates.json +1 -1
- package/tools/ack-write.mjs +20 -11
- package/tools/atomic-write.mjs +71 -18
- package/tools/checker-claim.mjs +100 -0
- package/tools/coverage-producer.mjs +43 -6
- package/tools/direct-run.mjs +76 -0
- package/tools/doc-parity.mjs +34 -3
- package/tools/engine-source.mjs +12 -8
- package/tools/ensure-configs.mjs +141 -0
- package/tools/ensure-ops.mjs +284 -0
- package/tools/ensure-vocabulary.mjs +71 -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 +36 -11
- package/tools/gates-init.mjs +140 -25
- package/tools/hide-footprint.mjs +21 -3
- package/tools/lens-region.mjs +74 -23
- package/tools/orchestration-config.mjs +5 -3
- package/tools/orchestration-write.mjs +7 -0
- package/tools/procedures.mjs +64 -5
- package/tools/recommendations.mjs +384 -34
- package/tools/refresh-parity.mjs +263 -0
- package/tools/run-gates.mjs +8 -5
- package/tools/setup-backends.mjs +88 -77
- package/tools/source-size-check.mjs +310 -0
- package/tools/source-size-config.mjs +244 -0
- package/tools/source-size-core.mjs +59 -0
- package/tools/source-size-gate-cmd.mjs +27 -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/tracked-tree-census.mjs +102 -0
- package/tools/upgrade-runlist.mjs +92 -0
- package/tools/velocity-profile.mjs +24 -3
|
@@ -27,8 +27,9 @@
|
|
|
27
27
|
//
|
|
28
28
|
// Read-only: never writes, never commits, never runs a subscription CLI. The reused probes are all
|
|
29
29
|
// exported read-only surfaces of their owning tools (velocity/autonomy/doctor/backends/recipes/
|
|
30
|
-
// registry/sandbox-masks); the sandbox-masks and
|
|
31
|
-
// Dependency-free, Node >= 22. No side effects on import (the isDirectRun
|
|
30
|
+
// registry/sandbox-masks); the sandbox-masks, settings and tracked-tree-census probes may run
|
|
31
|
+
// read-only git queries. Dependency-free, Node >= 22. No side effects on import (the isDirectRun
|
|
32
|
+
// idiom).
|
|
32
33
|
|
|
33
34
|
import { readFileSync, readdirSync, lstatSync, existsSync } from 'node:fs';
|
|
34
35
|
import { createHash } from 'node:crypto';
|
|
@@ -55,7 +56,17 @@ import { surveyFamily, surveyGateHook, surveyAdrLayoutStrict } from './family-re
|
|
|
55
56
|
import { probeSandboxMasks, needsMasksApply } from './sandbox-masks.mjs';
|
|
56
57
|
import { shellQuoteArg } from './review-state.mjs';
|
|
57
58
|
import { isFinalCapableDeclaration } from './run-gates.mjs';
|
|
58
|
-
import { loadDeclaration, canonicalCheckerGates, coverageProducerPrecedes,
|
|
59
|
+
import { loadDeclaration, canonicalCheckerGates, coverageProducerPrecedes, isKitOwnedCheckerGate, GATES_REL, LCOV_PRODUCER_KEY } from './gates-declaration.mjs';
|
|
60
|
+
import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
|
|
61
|
+
import { matchesCoverageProducer, isCoverageProducerGate } from './coverage-producer.mjs';
|
|
62
|
+
// How much of the TRACKED tree the changed-line coverage domain can assess at all — the fact that
|
|
63
|
+
// turns "the checker certifies" into "the checker certifies the assessable minority".
|
|
64
|
+
import { takeCensus, censusFact, CENSUS_VERDICT } from './tracked-tree-census.mjs';
|
|
65
|
+
// Read-only surfaces of the fill (buildOffer) and of the source-size practice (its pure core). The
|
|
66
|
+
// fill's own WRITER is never called from here — the advisor renders its consent-gated command, it
|
|
67
|
+
// does not run it.
|
|
68
|
+
import { buildOffer } from './gates-init.mjs';
|
|
69
|
+
import { CHECKER_CLAIM, INITIAL_ADOPTION_REASON, SOURCE_SIZE_GATE_ID, classifySourceSizeGate, loadSourceSizeConfig } from './source-size-core.mjs';
|
|
59
70
|
// The declared-path resolution + segment containment this item's convergence lane shares with the
|
|
60
71
|
// autonomy render's allowWrite degrade — ONE leaf, so the two answers cannot drift.
|
|
61
72
|
import { resolveDeclaredDir, dirCovers, isResolvableDeclaredEntry } from './declared-paths.mjs';
|
|
@@ -106,7 +117,26 @@ export const SEVERITIES = Object.freeze({
|
|
|
106
117
|
'gates-declaration': SEVERITY_OPTIONAL,
|
|
107
118
|
'gates-inert': SEVERITY_ATTENTION,
|
|
108
119
|
'gates-inert.no-verification': SEVERITY_ATTENTION,
|
|
120
|
+
// Both third outcomes are attention, and both therefore block the flow-optimal line MECHANICALLY:
|
|
121
|
+
// an added item makes the verdict non-null. One reports a pair that is dead on a tree the closed
|
|
122
|
+
// producer world cannot speak for; the other reports a LIVE pair whose certification reaches only
|
|
123
|
+
// the assessable minority of that tree. Neither changes what a run may certify.
|
|
124
|
+
'gates-inert.producer-unrecognized': SEVERITY_ATTENTION,
|
|
125
|
+
'gates-inert.coverage-domain-narrow': SEVERITY_ATTENTION,
|
|
126
|
+
'source-size': SEVERITY_OPTIONAL,
|
|
127
|
+
// The declared-but-unminted arm reports a CONFIGURED declaration that is broken — a gate certain
|
|
128
|
+
// to refuse on every run — while the base arm stays an offer to enable something unconfigured.
|
|
129
|
+
'source-size.unminted': SEVERITY_ATTENTION,
|
|
130
|
+
// A vendored copy of this checker is a DELIBERATE deployment choice, not a broken declaration —
|
|
131
|
+
// the practice runs, the advisor simply cannot read it through its own tool; the offer is the
|
|
132
|
+
// acknowledgment. An id SQUATTER is the opposite: the id says adopted while nothing measures size.
|
|
133
|
+
'source-size.adopted-elsewhere': SEVERITY_OPTIONAL,
|
|
134
|
+
'source-size.id-squatter': SEVERITY_ATTENTION,
|
|
109
135
|
'gate-hook': SEVERITY_OPTIONAL,
|
|
136
|
+
// A placed hook that predates the marker key goes DARK on a marker-carrying declaration (unknown
|
|
137
|
+
// key → auto-approval off), so a CONFIGURED capability silently stops working: attention, not an
|
|
138
|
+
// offer to enable something.
|
|
139
|
+
'gate-hook.marker-stale': SEVERITY_ATTENTION,
|
|
110
140
|
'commit-guard': SEVERITY_OPTIONAL,
|
|
111
141
|
'read-lane': SEVERITY_OPTIONAL,
|
|
112
142
|
'read-lane.stale': SEVERITY_ATTENTION,
|
|
@@ -166,7 +196,14 @@ export const WHATS = Object.freeze({
|
|
|
166
196
|
'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
197
|
'gates-inert': 'the declared coverage checker ({id}) has no producer before it — it certifies nothing this run, or reads a stale lcov',
|
|
168
198
|
'gates-inert.no-verification': "all {n} declared gate(s) are the kit's own checkers — the matrix runs no project-verification command",
|
|
199
|
+
'gates-inert.producer-unrecognized': 'the coverage checker ({id}) has NO producer declared and none offerable, on a tree the changed-line domain barely reaches',
|
|
200
|
+
'gates-inert.coverage-domain-narrow': 'certification covers the assessable minority only — {ext} dominate(s) the tracked tree, and the changed-line domain excludes them',
|
|
201
|
+
'source-size': 'no source-size gate — module size drifts unmeasured, and an over-cap file is invisible instead of recorded debt',
|
|
202
|
+
'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',
|
|
203
|
+
'source-size.adopted-elsewhere': "the source-size gate runs a DIFFERENT copy of this checker ({n} declared) — adopted here, just not through this kit's own tool",
|
|
204
|
+
'source-size.id-squatter': 'a gate carries the source-size id but is not this checker — the practice reads as declared while nothing measures module size',
|
|
169
205
|
'gate-hook': '{n} declared gate(s) prompt per run — the gate-approval hook is not wired',
|
|
206
|
+
'gate-hook.marker-stale': 'the declaration carries the lcovProducer key but the placed hook predates it — the hook goes dark and every gate prompts',
|
|
170
207
|
'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
208
|
'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',
|
|
172
209
|
'read-lane.stale': 'the read-lane is ON but the placed gate hook is stale — an old hook never reads lanes.json, so the lane is silently dark; reseed it',
|
|
@@ -223,6 +260,7 @@ export const BENEFITS = Object.freeze({
|
|
|
223
260
|
'review-recipe': 'recipe coverage — the review AND execution recipes you configured actually run instead of silently degrading',
|
|
224
261
|
'gates-declaration': 'velocity — your project’s gates run as ONE declared batch with a PASS/FAIL table',
|
|
225
262
|
'gates-inert': 'honest gates — the declared matrix verifies your project instead of reporting green over a check that ran nothing',
|
|
263
|
+
'source-size': 'maintainability — a module you can hold whole stays reviewable, and size drift becomes recorded, reasoned debt',
|
|
226
264
|
'gate-hook': 'velocity — your own declared gate commands auto-approve byte-exactly (opt-in PreToolUse hook)',
|
|
227
265
|
'commit-guard': 'integrity — commits require the ONE green --final receipt at the exact staged fingerprint (consented pre-commit arm)',
|
|
228
266
|
'read-lane': 'velocity — pipes/chains of your seeded read-only commands auto-approve instead of prompting (opt-in, conservatively classified)',
|
|
@@ -263,6 +301,10 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
|
|
|
263
301
|
// converges the moment any gate exists, so it can never observe this state. The advisor key names
|
|
264
302
|
// the state it reports (an inert declaration), the capability names what the user gains.
|
|
265
303
|
{ id: 'gates-verification', mode: 'gates', advisorKey: 'gates-inert' },
|
|
304
|
+
// The source-size practice is accepted AS a gate — it needs no mode of its own, and its capability
|
|
305
|
+
// is therefore declared where its declaration lives. It is the DISCOVERY lane for the practice on
|
|
306
|
+
// every deployment, new and existing alike: nothing else tells a project the practice exists.
|
|
307
|
+
{ id: 'source-size', mode: 'gates', advisorKey: 'source-size' },
|
|
266
308
|
{ id: 'gate-hook', mode: 'hook', advisorKey: 'gate-hook' },
|
|
267
309
|
{ id: 'read-lane', mode: 'hook', advisorKey: 'read-lane' },
|
|
268
310
|
{ id: 'commit-guard', mode: 'commit-guard', advisorKey: 'commit-guard' },
|
|
@@ -402,7 +444,7 @@ const probeReviewRecipe = ({ root, deps, add, skip }) => {
|
|
|
402
444
|
}
|
|
403
445
|
};
|
|
404
446
|
|
|
405
|
-
const probeGates = ({ root, deps, add, skip }) => {
|
|
447
|
+
const probeGates = ({ root, deps, add, skip, shared }) => {
|
|
406
448
|
try {
|
|
407
449
|
const sg = surveyGateHook(root, deps);
|
|
408
450
|
if (sg.error) throw new Error(sg.error);
|
|
@@ -417,9 +459,32 @@ const probeGates = ({ root, deps, add, skip }) => {
|
|
|
417
459
|
add('gates-declaration', fillTemplate(WHATS['gates-declaration'], {}), `node ${q(toolPath('gates-init.mjs'))} --cwd ${q(root)}`);
|
|
418
460
|
return;
|
|
419
461
|
}
|
|
420
|
-
if (
|
|
462
|
+
if (!sg.wired) {
|
|
421
463
|
add('gate-hook', fillTemplate(WHATS['gate-hook'], { n: sg.declaredGates }), `node ${q(toolPath('gate-hook.mjs'))} --apply --cwd ${q(root)}`);
|
|
464
|
+
return;
|
|
422
465
|
}
|
|
466
|
+
// D8 — the marker REFRESH path. The placed hook validates the declaration through its OWN baked
|
|
467
|
+
// copy and goes dark on any key it does not know (auto-approval off, every gate prompts again),
|
|
468
|
+
// so a declaration carrying the marker key under a hook that predates it silently switches a
|
|
469
|
+
// CONFIGURED capability off with no error anywhere. Deliberately marker-SCOPED: a stale hook is
|
|
470
|
+
// otherwise harmless, and nagging every deployment about hook bytes is not this item's job.
|
|
471
|
+
if (!declarationCarriesMarker(root, deps)) return;
|
|
472
|
+
// Throws on a symlink / directory / unreadable target → the stated skip, never a wrong verdict.
|
|
473
|
+
// ABSENT is not this arm either: a hook that is not placed belongs to the place offers above and
|
|
474
|
+
// in the read-lane item, whose recovery actually places one.
|
|
475
|
+
if (readPlacedHookCurrency(root, deps) !== HOOK_CURRENCY.STALE) return;
|
|
476
|
+
// The writer's OWN stale recovery, in its exact shape: `gate-hook --apply` never overwrites a
|
|
477
|
+
// placed diverged hook (it places only an ABSENT target), so remove-then-reseed is the only lane
|
|
478
|
+
// that converges — and it is maintainer territory, hence HAND-APPLY.
|
|
479
|
+
// The RESULT is what the read-lane item defers to — not these conditions re-derived there. Both
|
|
480
|
+
// probes read the placed hook independently, so a hook that changes between the two reads would
|
|
481
|
+
// otherwise let each conclude the other owns it and leave a dark lane reported by nobody.
|
|
482
|
+
shared.markerStaleRendered = add(
|
|
483
|
+
'gate-hook',
|
|
484
|
+
fillTemplate(WHATS['gate-hook.marker-stale'], {}),
|
|
485
|
+
`HAND-APPLY: rm ${q(join(root, GATE_HOOK_REL))}, then node ${q(toolPath('gate-hook.mjs'))} --apply --cwd ${q(root)}`,
|
|
486
|
+
'gate-hook.marker-stale',
|
|
487
|
+
);
|
|
423
488
|
} catch (err) {
|
|
424
489
|
skip('gate-hook', err);
|
|
425
490
|
}
|
|
@@ -430,14 +495,34 @@ const probeGates = ({ root, deps, add, skip }) => {
|
|
|
430
495
|
// decide with — the checker side through canonicalCheckerGates, the producer side through the
|
|
431
496
|
// closed matchesCoverageProducer — so the advisor can never disagree with what --final accepts.
|
|
432
497
|
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
// the
|
|
498
|
+
// A declared CHECKER is answered first and alone, in the arms below: the DOMAIN arm when the pair is
|
|
499
|
+
// live, the marker arm when no producer exists anywhere on a tree the domain cannot reach, and cause
|
|
500
|
+
// A — the dead or mis-ordered pair — otherwise. Its remedy also resolves cause B, because a producer
|
|
501
|
+
// gate is not a kit checker. Cause A's apply follows what the FILL can actually do, which the
|
|
502
|
+
// D-8 placement rule changed: when the checker is the declaration's LAST gate and the project's own
|
|
503
|
+
// scripts yield an offerable producer, the fill now PLACES that producer before the checker, so the
|
|
504
|
+
// remedy is the ordinary consent-gated preview. HAND-APPLY remains exactly where no offerable
|
|
505
|
+
// producer exists, or where the checker is not last — there the edit really is the maintainer's,
|
|
506
|
+
// because the fill never reorders entries it did not write.
|
|
438
507
|
//
|
|
439
508
|
// An ABSENT or EMPTY declaration belongs to the gates-declaration item; a malformed one throws out
|
|
440
509
|
// of the validated reader and becomes this probe's stated skip, never a guess.
|
|
510
|
+
// ONE rule for every fill preview this item renders: name only the entries the fill would ACCEPT.
|
|
511
|
+
// An id the declaration already carries is refused as a collision, so an unrestricted preview hands
|
|
512
|
+
// the reader a lane that cannot converge — and both arms of this item render over a declaration that
|
|
513
|
+
// already carries at least one gate the offer also proposes, so both need the rule. With nothing
|
|
514
|
+
// selectable the bare preview is still the honest render: its own notes say why.
|
|
515
|
+
const fillPreviewFor = (root, ids) =>
|
|
516
|
+
`node ${q(toolPath('gates-init.mjs'))} --cwd ${q(root)}${ids.map((id) => ` --only ${id}`).join('')}`;
|
|
517
|
+
|
|
518
|
+
// The census, injectable WHOLE and only whole. Forwarding this probe's `deps` into the leaf would
|
|
519
|
+
// hand it whatever `spawn` another probe's fixture happens to inject, so a masks-probe stub would
|
|
520
|
+
// silently become this probe's git — the census takes its own default and a test replaces the
|
|
521
|
+
// function, never its seams. It THROWS on an unavailable tree; each call site below decides what
|
|
522
|
+
// that means there, because the two arms have different honest answers and one global fallback
|
|
523
|
+
// would be wrong for one of them.
|
|
524
|
+
const readCensus = (root, deps) => (deps.takeCensus ?? takeCensus)(root);
|
|
525
|
+
|
|
441
526
|
export const probeGatesInert = ({ root, deps, add, skip }) => {
|
|
442
527
|
try {
|
|
443
528
|
const declaration = loadDeclaration(root, deps);
|
|
@@ -449,20 +534,135 @@ export const probeGatesInert = ({ root, deps, add, skip }) => {
|
|
|
449
534
|
// ORDER decides, through the declaration's own shared predicate: a producer declared AFTER the
|
|
450
535
|
// checker leaves it just as inert as no producer at all (it reads nothing, or stale bytes), and
|
|
451
536
|
// only --final refuses that shape — a plain run reports every gate PASS.
|
|
452
|
-
if (coverageProducerPrecedes(gates, gates.indexOf(checkers[0])))
|
|
537
|
+
if (coverageProducerPrecedes(gates, gates.indexOf(checkers[0]))) {
|
|
538
|
+
// The pair is LIVE, and that is exactly where the DOMAIN question becomes the honest one: the
|
|
539
|
+
// checker's changed-line domain is `.mjs/.cjs/.js` by design, so on a tree dominated by what
|
|
540
|
+
// that domain excludes, "certified" means "certified over the assessable minority". An
|
|
541
|
+
// UNAVAILABLE census throws out of here into the stated-skip lane — no census, no optimality
|
|
542
|
+
// claim; this is the ONE arm where nothing else would render, so a silent return would be the
|
|
543
|
+
// false green one layer down.
|
|
544
|
+
const census = readCensus(root, deps);
|
|
545
|
+
if (census.verdict !== CENSUS_VERDICT.NARROW) return; // the domain reaches this tree — converged
|
|
546
|
+
// The ack binds the FACT (verdict + the sorted unsupported extensions), never the counts: a
|
|
547
|
+
// count-bound ack would re-fire on every added file and turn the acknowledgment into a nag.
|
|
548
|
+
const fingerprint = factFingerprint(censusFact(census));
|
|
549
|
+
if (readAckValue(root, deps, ACKS_COVERAGE_DOMAIN_KEY) === fingerprint) return; // acknowledged
|
|
550
|
+
const ext = capList(census.unsupportedExtensions, templateBudget(WHATS['gates-inert.coverage-domain-narrow']), ', ');
|
|
551
|
+
add(
|
|
552
|
+
'gates-inert',
|
|
553
|
+
fillTemplate(WHATS['gates-inert.coverage-domain-narrow'], { ext }),
|
|
554
|
+
`node ${q(toolPath('ack-write.mjs'))} --lane coverage-domain --fingerprint ${fingerprint} --cwd ${q(root)}`,
|
|
555
|
+
'gates-inert.coverage-domain-narrow',
|
|
556
|
+
);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
// The fill's own answer is needed BEFORE the arms split, because one of them makes a claim
|
|
560
|
+
// about it. The PRODUCER entry is kept, not just its existence: a rendered preview must name it
|
|
561
|
+
// with --only. A whole-offer apply collides by construction — the declaration already carries
|
|
562
|
+
// the checker, and the offer carries it too — so an unrestricted preview would hand the reader
|
|
563
|
+
// a lane that refuses instead of the one entry that resolves what the item just reported.
|
|
564
|
+
// Read once and asked TWO different questions, because the two arms need different ones. The
|
|
565
|
+
// fill's selectable entry is collision-filtered: an id the declaration already carries is
|
|
566
|
+
// refused, so counting it would render a preview that cannot fix what the item just reported.
|
|
567
|
+
// The EXPRESSIBILITY question is not filtered at all — a `node --test` script whose offered id
|
|
568
|
+
// happens to be taken is still a suite the recognized producer set can express, and the narrow
|
|
569
|
+
// arm's sentence would be false over it. An offer computation that throws reaches the probe's
|
|
570
|
+
// stated-skip lane, which is the honest answer to "can the fill help here" when nobody knows.
|
|
571
|
+
const declaredIds = new Set(gates.map((gate) => gate.id));
|
|
572
|
+
const offered = buildOffer(root, deps).entries.filter((entry) => matchesCoverageProducer(entry.cmd));
|
|
573
|
+
const producer = offered.find((entry) => !declaredIds.has(entry.id));
|
|
574
|
+
// The dead pair on a tree the recognized producer set cannot speak for. It is deliberately NOT
|
|
575
|
+
// the ordering arm: a producer that EXISTS but sits after the checker is one MOVE away from
|
|
576
|
+
// working, and prescribing the marker there would teach the wrong fix — so this arm requires
|
|
577
|
+
// no producer ANYWHERE in the declaration. The ack lane is closed to it on purpose: a dead pair
|
|
578
|
+
// is broken, not narrow, and removing a producer after an acknowledgment lands right back here.
|
|
579
|
+
//
|
|
580
|
+
// The canonical checker ROWS are excluded from that search, and the exclusion is load-bearing:
|
|
581
|
+
// producer-ness is POSITIONAL everywhere else in this family precisely so a marker on the
|
|
582
|
+
// checker cannot self-pair, and an "anywhere" test without the exclusion re-opens exactly that
|
|
583
|
+
// hole — a checker carrying its own marker would read as its own producer and route a dead pair
|
|
584
|
+
// into the ordering arm.
|
|
585
|
+
//
|
|
586
|
+
// An OFFERED producer also disqualifies this arm, and that is the sharper condition of the
|
|
587
|
+
// two. The census answers how much of the tree the coverage DOMAIN reaches; it says nothing
|
|
588
|
+
// about producers at all. A TS-dominated project whose package.json still carries a
|
|
589
|
+
// `node --test` script has both facts at once, and the arm must not fire over it. The test is
|
|
590
|
+
// the UNFILTERED offer, not the fill's selectable entry: a collision on the offered id blocks
|
|
591
|
+
// the preview without making the producer one bit less real.
|
|
592
|
+
//
|
|
593
|
+
// STATED RESIDUAL, and the reason this arm no longer claims INEXPRESSIBILITY: the fill screens
|
|
594
|
+
// by terminating-class script NAME before it ever looks at a body, so `"ci": "node --test"` is
|
|
595
|
+
// a recognizable producer the offer never carries. The arm therefore says only what it knows —
|
|
596
|
+
// nothing declares one and nothing offers one — and its apply names that gap, because the
|
|
597
|
+
// remedy there is a hand-declared gate rather than the marker.
|
|
598
|
+
if (offered.length === 0 && !gates.some((gate) => !checkers.includes(gate) && isCoverageProducerGate(gate))) {
|
|
599
|
+
// An UNAVAILABLE census is NOT a skip here. The dead pair is a defect on any tree, and the
|
|
600
|
+
// arm below states it truthfully — falling through keeps a non-git deployment byte-identical
|
|
601
|
+
// to what it saw before this outcome existed, and still renders an item, so the flow-optimal
|
|
602
|
+
// line stays blocked either way. Only THAT failure is absorbed: the census leaf tags its own
|
|
603
|
+
// unavailability, and a bug reaching here would otherwise be laundered into an ordinary
|
|
604
|
+
// diagnosis instead of surfacing.
|
|
605
|
+
const narrow = (() => {
|
|
606
|
+
try {
|
|
607
|
+
return readCensus(root, deps).verdict === CENSUS_VERDICT.NARROW;
|
|
608
|
+
} catch (err) {
|
|
609
|
+
if (err?.code === 'CENSUS_UNAVAILABLE') return false;
|
|
610
|
+
throw err;
|
|
611
|
+
}
|
|
612
|
+
})();
|
|
613
|
+
if (narrow) {
|
|
614
|
+
const id = truncatedTo(oneLineOf(checkers[0].id), templateBudget(WHATS['gates-inert.producer-unrecognized']));
|
|
615
|
+
add(
|
|
616
|
+
'gates-inert',
|
|
617
|
+
fillTemplate(WHATS['gates-inert.producer-unrecognized'], { id }),
|
|
618
|
+
`HAND-APPLY: mark the gate that actually writes the lcov with "lcovProducer": true in ${GATES_REL} (references/modes/gates.md names the key), or drop ${id} — and note the fill offers only terminating-class script NAMES, so a recognized body under another name (a "ci" script running node --test) is declared by hand, not marked`,
|
|
619
|
+
'gates-inert.producer-unrecognized',
|
|
620
|
+
);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
453
624
|
const id = truncatedTo(oneLineOf(checkers[0].id), templateBudget(WHATS['gates-inert']));
|
|
625
|
+
// The fill can only help when it would land the producer in the right place: the checker must
|
|
626
|
+
// be LAST (that is the one position the placement rule inserts before) and the offer must
|
|
627
|
+
// actually carry a producer the fill would ACCEPT (computed above).
|
|
628
|
+
const checkerIsLast = checkers[0] === gates[gates.length - 1];
|
|
629
|
+
// THREE ways the fill cannot help, and each needs its own sentence, because each names a
|
|
630
|
+
// different edit for the reader to make. A checker that is not last blocks a placement even
|
|
631
|
+
// when a producer is offerable — that is about POSITION, so it leads. An offered producer whose
|
|
632
|
+
// id is already taken is not an absent one: the suite is expressible and the fill is merely
|
|
633
|
+
// refused on the collision, so naming the conflicting id turns a dead end into a rename. Only
|
|
634
|
+
// with neither of those is "no offerable producer exists here" a true sentence.
|
|
635
|
+
const colliding = producer === undefined ? offered.find((entry) => declaredIds.has(entry.id)) : undefined;
|
|
636
|
+
const blocked = !checkerIsLast
|
|
637
|
+
? `${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`
|
|
638
|
+
: colliding !== undefined
|
|
639
|
+
? `a producer IS offerable here, but its id "${colliding.id}" is already declared, so the fill refuses it as a collision — rename that gate, or repoint it at the producer form`
|
|
640
|
+
: `no offerable producer exists here, and the fill never reorders entries it did not write`;
|
|
454
641
|
add(
|
|
455
642
|
'gates-inert',
|
|
456
643
|
fillTemplate(WHATS['gates-inert'], { id }),
|
|
457
|
-
|
|
644
|
+
checkerIsLast && producer
|
|
645
|
+
? fillPreviewFor(root, [producer.id])
|
|
646
|
+
: `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
647
|
);
|
|
459
648
|
return;
|
|
460
649
|
}
|
|
461
|
-
|
|
650
|
+
// The source-size checker is one of the kit's OWN checkers, and it is deliberately NOT
|
|
651
|
+
// review-dependent (it needs no receipt), so the review-dependent predicate alone cannot see it.
|
|
652
|
+
// Left out, a matrix of nothing but that gate reads as carrying project verification — and a
|
|
653
|
+
// project that just adopted the practice and declared nothing else would be told it is optimal.
|
|
654
|
+
if (gates.every((gate) => isKitOwnedCheckerGate(gate, root))) {
|
|
655
|
+
const declaredIds = new Set(gates.map((gate) => gate.id));
|
|
656
|
+
// Only PROJECT-verification entries: a non-colliding entry is not enough, it has to be one
|
|
657
|
+
// that RESOLVES the item, and declaring one more kit checker converges nothing — the item
|
|
658
|
+
// would simply fire again on the next run.
|
|
659
|
+
const selectable = buildOffer(root, deps).entries
|
|
660
|
+
.filter((entry) => !declaredIds.has(entry.id) && !isKitOwnedCheckerGate(entry, root))
|
|
661
|
+
.map((entry) => entry.id);
|
|
462
662
|
add(
|
|
463
663
|
'gates-inert',
|
|
464
664
|
fillTemplate(WHATS['gates-inert.no-verification'], { n: gates.length }),
|
|
465
|
-
|
|
665
|
+
fillPreviewFor(root, selectable),
|
|
466
666
|
'gates-inert.no-verification',
|
|
467
667
|
);
|
|
468
668
|
}
|
|
@@ -471,6 +671,89 @@ export const probeGatesInert = ({ root, deps, add, skip }) => {
|
|
|
471
671
|
}
|
|
472
672
|
};
|
|
473
673
|
|
|
674
|
+
// The SOURCE-SIZE offer (baseline-practices Plan 1). This is the practice's ONE discovery lane: the
|
|
675
|
+
// checker ships with the kit and refuses until a project declares its own scope, so without this
|
|
676
|
+
// item a deployment would never learn the practice exists — the same OPT-IN-SHIPS-INVISIBLE failure
|
|
677
|
+
// the capability registry was built for. New and existing deployments meet it identically, because
|
|
678
|
+
// the advisor section is mandatory at every upgrade.
|
|
679
|
+
//
|
|
680
|
+
// The probe asks what each declared cmd CLAIMS about this checker, in the three named outcomes —
|
|
681
|
+
// because the boolean it used to ask made a DELIBERATELY VENDORED copy read as "no source-size gate
|
|
682
|
+
// at all", and the remedy that false absence rendered (`--adopt`) then collides on the very id
|
|
683
|
+
// already in the declaration. It deliberately does NOT key on the config's state to decide WHETHER
|
|
684
|
+
// to speak: a project with no config is exactly the project that needs to hear about the practice,
|
|
685
|
+
// and the apply's own refusal is what teaches the one manual step (authoring the scope). A missing
|
|
686
|
+
// declaration is not a skip — there is no source-size gate in it either.
|
|
687
|
+
//
|
|
688
|
+
// Precedence over a MIXED declaration is pinned and deterministic: any canonical match answers the
|
|
689
|
+
// practice question outright; else any tool-elsewhere claim; else the id collision; else the offer.
|
|
690
|
+
const probeSourceSize = ({ root, deps, add, skip }) => {
|
|
691
|
+
try {
|
|
692
|
+
const declaration = loadDeclaration(root, deps);
|
|
693
|
+
const gates = declaration.outcome === 'loaded' ? declaration.gates : [];
|
|
694
|
+
const tool = toolPath('source-size-check.mjs');
|
|
695
|
+
// The reason string is PINNED, not composed: it is copied unchanged into every entry the first
|
|
696
|
+
// mint records, and a first mint records the whole tree — so "this is what the tree already
|
|
697
|
+
// carried when the practice arrived" is the one sentence that is true of all of them.
|
|
698
|
+
const adoptLine = `node ${q(tool)} --adopt --reason "${INITIAL_ADOPTION_REASON}" --cwd ${q(root)}`;
|
|
699
|
+
const claims = gates.map((gate) => ({ gate, claim: classifySourceSizeGate(gate.cmd, root) }));
|
|
700
|
+
const canonical = claims.filter((c) => c.claim === CHECKER_CLAIM.CANONICAL);
|
|
701
|
+
const elsewhere = claims.filter((c) => c.claim === CHECKER_CLAIM.ELSEWHERE);
|
|
702
|
+
if (canonical.length > 0 || elsewhere.length > 0) {
|
|
703
|
+
// A DECLARED gate is not the same fact as a working one: the checker refuses on every config
|
|
704
|
+
// state but MINTED, so a gate declared over an absent or half-written record reds the matrix
|
|
705
|
+
// on every run. Reading the declaration alone would report that deployment as adopted and say
|
|
706
|
+
// nothing about the one thing that is wrong with it. The split changes NOTHING here — the
|
|
707
|
+
// outcome, its attention class and its immunity to every ack are the same whichever copy runs.
|
|
708
|
+
const { state } = loadSourceSizeConfig(root, deps);
|
|
709
|
+
if (state !== 'minted') {
|
|
710
|
+
// Only the way OUT differs. With the gate declared through another copy, `--adopt` would
|
|
711
|
+
// mint the record and then be refused by the fill on the id it is already looking at, so the
|
|
712
|
+
// rendered verb is the mint alone: the declaration is not the half that is missing.
|
|
713
|
+
const mintLine = `node ${q(tool)} --write-baseline --reason "${INITIAL_ADOPTION_REASON}" --cwd ${q(root)}`;
|
|
714
|
+
add(
|
|
715
|
+
'source-size',
|
|
716
|
+
fillTemplate(WHATS['source-size.unminted'], { state }),
|
|
717
|
+
canonical.length > 0 ? adoptLine : mintLine,
|
|
718
|
+
'source-size.unminted',
|
|
719
|
+
);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (canonical.length > 0) return; // adopted through THIS copy and armed — converged
|
|
723
|
+
// A vendored copy is a deployment CHOICE, not a defect: the practice runs, the realpath anchor
|
|
724
|
+
// simply cannot see it as this advisor's own sibling (and never widens — that anchor is what
|
|
725
|
+
// keeps a lookalike from certifying). So the convergence is an acknowledgment, never `--adopt`.
|
|
726
|
+
// The fingerprint binds the claims as AUTHORED, sorted and de-duplicated — not the resolved
|
|
727
|
+
// realpaths, which are machine-specific and would churn a committed ack between machines.
|
|
728
|
+
const fingerprint = factFingerprint(JSON.stringify([...new Set(elsewhere.map((c) => c.gate.cmd))].sort()));
|
|
729
|
+
if (readAckValue(root, deps, ACKS_SOURCE_SIZE_COPY_KEY) === fingerprint) return; // acknowledged
|
|
730
|
+
add(
|
|
731
|
+
'source-size',
|
|
732
|
+
fillTemplate(WHATS['source-size.adopted-elsewhere'], { n: elsewhere.length }),
|
|
733
|
+
`node ${q(toolPath('ack-write.mjs'))} --lane source-size-copy --fingerprint ${fingerprint} --cwd ${q(root)}`,
|
|
734
|
+
'source-size.adopted-elsewhere',
|
|
735
|
+
);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
// The id SQUATTER — the id says the practice is declared, the cmd is not this checker under any
|
|
739
|
+
// reading. `--adopt` here mints the record and is then refused by the fill on the id collision:
|
|
740
|
+
// correct against a squatter, and useless as a RENDERED remedy, so this arm names the two hand
|
|
741
|
+
// edits that actually resolve it instead.
|
|
742
|
+
if (gates.some((gate) => gate.id === SOURCE_SIZE_GATE_ID)) {
|
|
743
|
+
add(
|
|
744
|
+
'source-size',
|
|
745
|
+
fillTemplate(WHATS['source-size.id-squatter'], {}),
|
|
746
|
+
`HAND-APPLY: in ${GATES_REL}, either rename the "${SOURCE_SIZE_GATE_ID}" gate to an id of its own, or repoint its cmd at node ${q(tool)} --check — then re-run recommendations`,
|
|
747
|
+
'source-size.id-squatter',
|
|
748
|
+
);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
add('source-size', fillTemplate(WHATS['source-size'], {}), adoptLine);
|
|
752
|
+
} catch (err) {
|
|
753
|
+
skip('source-size', err);
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
|
|
474
757
|
// The D10 consumer surface: once the declaration is FINAL-capable (the canonical core checks
|
|
475
758
|
// present, the checker LAST — the run-gates helper is the one home of that rule), offer the
|
|
476
759
|
// consented commit-guard install — for a MANAGED guardless pre-commit hook AND for an absent one
|
|
@@ -527,15 +810,17 @@ const probeCommitGuard = ({ root, deps, add, skip }) => {
|
|
|
527
810
|
// or un-wired hook is covered there, never double-offered here. The apply is the gate-hook
|
|
528
811
|
// --read-lane PREVIEW one-liner (its own currency check + posture note fire at the writer; it may
|
|
529
812
|
// prompt once — it IS a consent flow). Converges once lanes.json enables the lane.
|
|
530
|
-
const probeReadLane = ({ root, deps, add, skip }) => {
|
|
813
|
+
const probeReadLane = ({ root, deps, add, skip, shared }) => {
|
|
531
814
|
try {
|
|
532
815
|
const sg = surveyGateHook(root, deps);
|
|
533
816
|
if (sg.error) throw new Error(sg.error);
|
|
534
817
|
if (!sg.wired) return; // not wired → the gate-hook item covers (no double-fire)
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
818
|
+
// Wired but the placed hook FILE is missing — the hook errors on every Bash call and the lane
|
|
819
|
+
// is silently dark; surface it as attention with a place-first recovery (council R2-M2).
|
|
820
|
+
const placeRecovery = () =>
|
|
538
821
|
add('read-lane', fillTemplate(WHATS['read-lane.missing'], {}), `node ${q(toolPath('gate-hook.mjs'))} --apply --cwd ${q(root)}`, 'read-lane.missing');
|
|
822
|
+
if (!sg.filePlaced) {
|
|
823
|
+
placeRecovery();
|
|
539
824
|
return;
|
|
540
825
|
}
|
|
541
826
|
if (readReadLaneToggle(root, deps)) {
|
|
@@ -543,7 +828,23 @@ const probeReadLane = ({ root, deps, add, skip }) => {
|
|
|
543
828
|
// reads lanes.json, so the enabled lane is a silent no-op the user must reseed (council B7). The
|
|
544
829
|
// rm target is ABSOLUTE (council R2-M3) so running the recovery from any cwd can only delete this
|
|
545
830
|
// repo's hook.
|
|
546
|
-
|
|
831
|
+
const currency = readPlacedHookCurrency(root, deps);
|
|
832
|
+
if (currency === HOOK_CURRENCY.CURRENT) return; // converged
|
|
833
|
+
if (currency === HOOK_CURRENCY.ABSENT) {
|
|
834
|
+
// The survey saw it placed and it is gone by the time we read it. The missing arm is the
|
|
835
|
+
// honest report of that; a reseed recovery whose `rm` targets nothing is not.
|
|
836
|
+
placeRecovery();
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
// ONE render per condition — and the item that renders is the one whose CAUSE is true. A hook
|
|
840
|
+
// that postdates the read-lane but predates the marker key reads lanes.json perfectly well, so
|
|
841
|
+
// this arm's "an old hook never reads lanes.json" would be a false diagnosis over it, while the
|
|
842
|
+
// marker arm's is exact. Both carry the same remove-then-reseed recovery, so deferring costs
|
|
843
|
+
// the reader nothing and buys a true sentence. The condition is that the marker arm REALLY
|
|
844
|
+
// rendered — never the conditions it would have used, re-derived here: each probe reads the
|
|
845
|
+
// placed hook itself, so a hook changing between the two reads could otherwise have both defer
|
|
846
|
+
// to the other and leave the dark lane reported by nobody.
|
|
847
|
+
if (shared.markerStaleRendered === true) return;
|
|
547
848
|
add(
|
|
548
849
|
'read-lane',
|
|
549
850
|
fillTemplate(WHATS['read-lane.stale'], {}),
|
|
@@ -725,6 +1026,13 @@ export const recipeFingerprint = ({ hosts, dirs, home }) => {
|
|
|
725
1026
|
return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
|
|
726
1027
|
};
|
|
727
1028
|
|
|
1029
|
+
// The fingerprint for an acknowledgment whose subject is already a canonical STRING — the census
|
|
1030
|
+
// fact, the sorted set of declared tool-elsewhere claims — rather than a hosts ∪ dirs recipe. Same
|
|
1031
|
+
// 16-hex shape the ack writer validates, so every lane records one comparable token; the canonical
|
|
1032
|
+
// form is the caller's, because only the caller knows which part of its fact is durable and which
|
|
1033
|
+
// is churn (the census binds the verdict + extension set, never per-file counts).
|
|
1034
|
+
export const factFingerprint = (fact) => createHash('sha256').update(fact).digest('hex').slice(0, 16);
|
|
1035
|
+
|
|
728
1036
|
// The kit-owned neutral ack store (D4; AD-055 Part I): a FAMILY-OWNED strict-JSON file no host
|
|
729
1037
|
// validator guards — top-level key `sandboxLaneAck` (+ optional `_README`), unknown keys tolerated
|
|
730
1038
|
// on read (future acks are siblings). This is the PRIMARY ack channel; the legacy settings-scope
|
|
@@ -733,12 +1041,21 @@ export const recipeFingerprint = ({ hosts, dirs, home }) => {
|
|
|
733
1041
|
export const ACKS_FILE = 'docs/ai/acks.json';
|
|
734
1042
|
export const ACKS_LANE_KEY = 'sandboxLaneAck';
|
|
735
1043
|
export const ACKS_WORKTREES_DIR_KEY = 'worktreesDirAck';
|
|
1044
|
+
export const ACKS_COVERAGE_DOMAIN_KEY = 'coverageDomainAck';
|
|
1045
|
+
export const ACKS_SOURCE_SIZE_COPY_KEY = 'sourceSizeCopyAck';
|
|
736
1046
|
// The CLOSED-WORLD ack-lane registry: the lane name an advisor item renders on the writer's
|
|
737
1047
|
// command line → the store key that writer sets. A lane the registry does not name is a usage
|
|
738
1048
|
// refusal at the writer, never a newly-invented key in the shared store.
|
|
1049
|
+
//
|
|
1050
|
+
// An ack lane exists for a state the maintainer can only ANSWER, never converge: a tracked tree the
|
|
1051
|
+
// coverage domain cannot reach, a checker deliberately vendored elsewhere. It is deliberately NOT
|
|
1052
|
+
// available to a state that is simply BROKEN — a dead checker/producer pair is fixed, not
|
|
1053
|
+
// acknowledged, so no lane names it.
|
|
739
1054
|
export const ACK_LANES = Object.freeze({
|
|
740
1055
|
'sandbox-lane': ACKS_LANE_KEY,
|
|
741
1056
|
'worktrees-dir': ACKS_WORKTREES_DIR_KEY,
|
|
1057
|
+
'coverage-domain': ACKS_COVERAGE_DOMAIN_KEY,
|
|
1058
|
+
'source-size-copy': ACKS_SOURCE_SIZE_COPY_KEY,
|
|
742
1059
|
});
|
|
743
1060
|
|
|
744
1061
|
// The opt-in read-lane toggle file (AD-055 Part II) — the SAME kit-owned docs/ai/lanes.json the
|
|
@@ -775,13 +1092,33 @@ const isStateBlockGuardWired = (data) => {
|
|
|
775
1092
|
: runsStateBlockGuard(entry)));
|
|
776
1093
|
};
|
|
777
1094
|
|
|
778
|
-
//
|
|
779
|
-
//
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1095
|
+
// The placed gate hook's currency, as the states a caller must tell APART (D8). Read through the
|
|
1096
|
+
// writer-class fail-closed no-follow primitive, never a bare readFile: a byte-compare that FOLLOWS a
|
|
1097
|
+
// symlink can call a placed hook current because something ELSE is — a wrong verdict, which is worse
|
|
1098
|
+
// than a missing one. `current` and `stale` are answers; `absent` is one the caller disposes of
|
|
1099
|
+
// itself (a hook that is not there belongs to the place offers, never to a refresh arm); a symlink,
|
|
1100
|
+
// a directory and an unreadable target all THROW into the probe's stated-skip lane.
|
|
1101
|
+
const HOOK_CURRENCY = Object.freeze({ CURRENT: 'current', STALE: 'stale', ABSENT: 'absent' });
|
|
1102
|
+
const readPlacedHookCurrency = (root, deps) => {
|
|
1103
|
+
const read = deps.readRegularFileNoFollow ?? readRegularFileNoFollow;
|
|
1104
|
+
const placed = read(join(root, GATE_HOOK_REL));
|
|
1105
|
+
if (placed.outcome === 'absent') return HOOK_CURRENCY.ABSENT;
|
|
1106
|
+
if (placed.outcome === 'foreign') {
|
|
1107
|
+
throw new Error(`${GATE_HOOK_REL} is a ${placed.className}, not a regular file — refusing to judge its currency`);
|
|
1108
|
+
}
|
|
1109
|
+
if (placed.outcome !== 'ok') throw new Error(`${GATE_HOOK_REL} is unreadable (${placed.code})`);
|
|
1110
|
+
const bundle = (deps.readFile ?? readFileSync)(deps.bundledHookPath ?? BUNDLED_HOOK_ABS, 'utf8');
|
|
1111
|
+
return placed.content === bundle ? HOOK_CURRENCY.CURRENT : HOOK_CURRENCY.STALE;
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
// Does the declaration carry the marker KEY at all? The question is PRESENCE, never the value: an
|
|
1115
|
+
// older hook rejects a key it does not know whatever that key says, so `lcovProducer: false` — a
|
|
1116
|
+
// perfectly valid declaration the runner accepts — darkens such a hook exactly as `true` does. Asking
|
|
1117
|
+
// through the producer predicate would be wrong twice over: it answers yes for a cmd-recognized
|
|
1118
|
+
// producer that needs nothing from the hook, and no for the false marker that does.
|
|
1119
|
+
const declarationCarriesMarker = (root, deps) => {
|
|
1120
|
+
const declaration = loadDeclaration(root, deps);
|
|
1121
|
+
return declaration.outcome === 'loaded' && declaration.gates.some((gate) => Object.hasOwn(gate, LCOV_PRODUCER_KEY));
|
|
785
1122
|
};
|
|
786
1123
|
|
|
787
1124
|
// The LEGACY neutral ack namespace (pre-AD-055): read from BOTH settings scopes until the next kit
|
|
@@ -851,7 +1188,7 @@ const readReadLaneToggle = (root, deps) => {
|
|
|
851
1188
|
// D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
|
|
852
1189
|
// at the consent moment; the static contract test asserts EXACT bidirectional coverage
|
|
853
1190
|
// (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
|
|
854
|
-
export const RISK_NOTED_KEYS = Object.freeze(['sandbox-lane', 'read-lane', 'worktrees-dir', 'adr-store-migration', 'gates-inert']);
|
|
1191
|
+
export const RISK_NOTED_KEYS = Object.freeze(['sandbox-lane', 'read-lane', 'worktrees-dir', 'adr-store-migration', 'gates-inert', 'source-size', 'gate-hook']);
|
|
855
1192
|
|
|
856
1193
|
const probeSandboxLane = ({ root, deps, add, skip }) => {
|
|
857
1194
|
try {
|
|
@@ -1054,6 +1391,7 @@ const PROBES = Object.freeze([
|
|
|
1054
1391
|
probeReviewRecipe,
|
|
1055
1392
|
probeGates,
|
|
1056
1393
|
probeGatesInert,
|
|
1394
|
+
probeSourceSize,
|
|
1057
1395
|
probeCommitGuard,
|
|
1058
1396
|
probeReadLane,
|
|
1059
1397
|
probeStateBlockHook,
|
|
@@ -1074,27 +1412,39 @@ export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
|
|
|
1074
1412
|
const skip = (key, err) => skips.push({ key, reason: truncatedTo(oneLineOf(err?.message ?? String(err)), SKIP_REASON_CAP) });
|
|
1075
1413
|
// The runtime shape backstop (D2): every COMPOSED item is validated at construction — a
|
|
1076
1414
|
// violation surfaces through the stated-skip lane, never a crash, never a rendered violation.
|
|
1077
|
-
//
|
|
1078
|
-
//
|
|
1415
|
+
// `variant` defaults to the item key; a per-site arm passes its `<key>.<variant>` entry. It is
|
|
1416
|
+
// BOTH the severity lookup and the machine-readable outcome identifier: the human render says
|
|
1417
|
+
// which item fired, and only this field says which ARM of it did — so a consumer (the pre-publish
|
|
1418
|
+
// smoke asserting a specific false-green never returns) can assert the exact outcome instead of
|
|
1419
|
+
// pattern-matching prose that is free to be reworded.
|
|
1079
1420
|
// `detail` (optional) is an extra rendered `recipe:` line — factual context that is TOO LONG for
|
|
1080
1421
|
// the capped WHAT and does NOT belong in the pure-command apply (the sandbox-lane live recipe:
|
|
1081
1422
|
// egress hosts + resolved writable dirs; the worktrees-dir hand-apply-first grant advice; the
|
|
1082
1423
|
// agents hidden-mode reconcile follow-up). Single-line like apply; absent for every other item.
|
|
1083
|
-
|
|
1424
|
+
// Returns whether the item really RENDERED. One probe's disposition depends on another's having
|
|
1425
|
+
// spoken (the marker-stale ⟷ read-lane.stale precedence), and "the conditions still look right"
|
|
1426
|
+
// is not the same fact as "an item exists" — the shape backstop can refuse, and a condition read
|
|
1427
|
+
// twice can answer twice.
|
|
1428
|
+
const add = (key, what, apply, variant = key, detail = null) => {
|
|
1084
1429
|
const problems = [];
|
|
1085
1430
|
if (!(key in BENEFITS)) problems.push(`unregistered item key ${JSON.stringify(key)}`);
|
|
1086
|
-
if (!(
|
|
1431
|
+
if (!(variant in SEVERITIES)) problems.push(`unregistered severity key ${JSON.stringify(variant)}`);
|
|
1087
1432
|
if (/[\r\n]/.test(what)) problems.push('WHAT is not a single line');
|
|
1088
1433
|
else if (what.length > ITEM_LINE_CAP) problems.push(`WHAT exceeds the ${ITEM_LINE_CAP}-char cap (${what.length})`);
|
|
1089
1434
|
if (/[\r\n]/.test(apply)) problems.push('apply is not a single line');
|
|
1090
1435
|
if (detail != null && /[\r\n]/.test(detail)) problems.push('recipe detail is not a single line');
|
|
1091
1436
|
if (problems.length > 0) {
|
|
1092
1437
|
skip(key, new Error(`item shape violation — ${problems.join('; ')}`));
|
|
1093
|
-
return;
|
|
1438
|
+
return false;
|
|
1094
1439
|
}
|
|
1095
|
-
items.push({ key, severity: SEVERITIES[
|
|
1440
|
+
items.push({ key, variant, severity: SEVERITIES[variant], what, benefit: BENEFITS[key], apply, detail });
|
|
1441
|
+
return true;
|
|
1096
1442
|
};
|
|
1097
|
-
|
|
1443
|
+
// The per-run scratch a probe uses to tell a LATER probe what it actually did. Written by exactly
|
|
1444
|
+
// one pair today (the marker-stale ⟷ read-lane.stale precedence) and read in the frozen PROBES
|
|
1445
|
+
// order, so the reader can never run first.
|
|
1446
|
+
const shared = {};
|
|
1447
|
+
for (const probe of deps.probes ?? PROBES) probe({ root, deps, add, skip, shared });
|
|
1098
1448
|
return { root, items, skips };
|
|
1099
1449
|
};
|
|
1100
1450
|
|