@sabaiway/agent-workflow-kit 5.6.0 → 5.8.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 +92 -0
- package/README.md +2 -2
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +26 -17
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +6 -5
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +13 -4
- package/bridges/antigravity-cli-bridge/bin/agy.sh +7 -4
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +24 -0
- package/bridges/antigravity-cli-bridge/capability.json +3 -3
- package/bridges/antigravity-cli-bridge/references/driving-agy.md +9 -8
- package/bridges/antigravity-cli-bridge/references/models-and-flags.md +31 -14
- package/bridges/antigravity-cli-bridge/setup/README.md +4 -3
- 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 +16 -3
- package/references/modes/grounding.md +4 -3
- package/references/modes/recommendations.md +3 -0
- package/references/modes/review-state.md +1 -1
- package/references/modes/setup.md +18 -2
- package/references/modes/upgrade.md +38 -18
- 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/gates-declaration.mjs +23 -10
- package/tools/gates-init.mjs +6 -3
- package/tools/grounding.mjs +105 -16
- 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/recommendations.mjs +315 -66
- 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 +6 -16
- package/tools/source-size-core.mjs +7 -1
- package/tools/source-size-gate-cmd.mjs +18 -46
- package/tools/tracked-tree-census.mjs +102 -0
- package/tools/upgrade-runlist.mjs +92 -0
|
@@ -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,13 +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, isKitOwnedCheckerGate, GATES_REL } from './gates-declaration.mjs';
|
|
59
|
-
import {
|
|
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';
|
|
60
65
|
// Read-only surfaces of the fill (buildOffer) and of the source-size practice (its pure core). The
|
|
61
66
|
// fill's own WRITER is never called from here — the advisor renders its consent-gated command, it
|
|
62
67
|
// does not run it.
|
|
63
68
|
import { buildOffer } from './gates-init.mjs';
|
|
64
|
-
import { INITIAL_ADOPTION_REASON,
|
|
69
|
+
import { CHECKER_CLAIM, INITIAL_ADOPTION_REASON, SOURCE_SIZE_GATE_ID, classifySourceSizeGate, loadSourceSizeConfig } from './source-size-core.mjs';
|
|
65
70
|
// The declared-path resolution + segment containment this item's convergence lane shares with the
|
|
66
71
|
// autonomy render's allowWrite degrade — ONE leaf, so the two answers cannot drift.
|
|
67
72
|
import { resolveDeclaredDir, dirCovers, isResolvableDeclaredEntry } from './declared-paths.mjs';
|
|
@@ -112,11 +117,26 @@ export const SEVERITIES = Object.freeze({
|
|
|
112
117
|
'gates-declaration': SEVERITY_OPTIONAL,
|
|
113
118
|
'gates-inert': SEVERITY_ATTENTION,
|
|
114
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,
|
|
115
126
|
'source-size': SEVERITY_OPTIONAL,
|
|
116
127
|
// The declared-but-unminted arm reports a CONFIGURED declaration that is broken — a gate certain
|
|
117
128
|
// to refuse on every run — while the base arm stays an offer to enable something unconfigured.
|
|
118
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,
|
|
119
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,
|
|
120
140
|
'commit-guard': SEVERITY_OPTIONAL,
|
|
121
141
|
'read-lane': SEVERITY_OPTIONAL,
|
|
122
142
|
'read-lane.stale': SEVERITY_ATTENTION,
|
|
@@ -176,9 +196,14 @@ export const WHATS = Object.freeze({
|
|
|
176
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',
|
|
177
197
|
'gates-inert': 'the declared coverage checker ({id}) has no producer before it — it certifies nothing this run, or reads a stale lcov',
|
|
178
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',
|
|
179
201
|
'source-size': 'no source-size gate — module size drifts unmeasured, and an over-cap file is invisible instead of recorded debt',
|
|
180
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',
|
|
181
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',
|
|
182
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',
|
|
183
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',
|
|
184
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',
|
|
@@ -419,7 +444,7 @@ const probeReviewRecipe = ({ root, deps, add, skip }) => {
|
|
|
419
444
|
}
|
|
420
445
|
};
|
|
421
446
|
|
|
422
|
-
const probeGates = ({ root, deps, add, skip }) => {
|
|
447
|
+
const probeGates = ({ root, deps, add, skip, shared }) => {
|
|
423
448
|
try {
|
|
424
449
|
const sg = surveyGateHook(root, deps);
|
|
425
450
|
if (sg.error) throw new Error(sg.error);
|
|
@@ -434,9 +459,32 @@ const probeGates = ({ root, deps, add, skip }) => {
|
|
|
434
459
|
add('gates-declaration', fillTemplate(WHATS['gates-declaration'], {}), `node ${q(toolPath('gates-init.mjs'))} --cwd ${q(root)}`);
|
|
435
460
|
return;
|
|
436
461
|
}
|
|
437
|
-
if (
|
|
462
|
+
if (!sg.wired) {
|
|
438
463
|
add('gate-hook', fillTemplate(WHATS['gate-hook'], { n: sg.declaredGates }), `node ${q(toolPath('gate-hook.mjs'))} --apply --cwd ${q(root)}`);
|
|
464
|
+
return;
|
|
439
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
|
+
);
|
|
440
488
|
} catch (err) {
|
|
441
489
|
skip('gate-hook', err);
|
|
442
490
|
}
|
|
@@ -447,9 +495,10 @@ const probeGates = ({ root, deps, add, skip }) => {
|
|
|
447
495
|
// decide with — the checker side through canonicalCheckerGates, the producer side through the
|
|
448
496
|
// closed matchesCoverageProducer — so the advisor can never disagree with what --final accepts.
|
|
449
497
|
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
//
|
|
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
|
|
453
502
|
// D-8 placement rule changed: when the checker is the declaration's LAST gate and the project's own
|
|
454
503
|
// scripts yield an offerable producer, the fill now PLACES that producer before the checker, so the
|
|
455
504
|
// remedy is the ordinary consent-gated preview. HAND-APPLY remains exactly where no offerable
|
|
@@ -466,6 +515,14 @@ const probeGates = ({ root, deps, add, skip }) => {
|
|
|
466
515
|
const fillPreviewFor = (root, ids) =>
|
|
467
516
|
`node ${q(toolPath('gates-init.mjs'))} --cwd ${q(root)}${ids.map((id) => ` --only ${id}`).join('')}`;
|
|
468
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
|
+
|
|
469
526
|
export const probeGatesInert = ({ root, deps, add, skip }) => {
|
|
470
527
|
try {
|
|
471
528
|
const declaration = loadDeclaration(root, deps);
|
|
@@ -477,27 +534,110 @@ export const probeGatesInert = ({ root, deps, add, skip }) => {
|
|
|
477
534
|
// ORDER decides, through the declaration's own shared predicate: a producer declared AFTER the
|
|
478
535
|
// checker leaves it just as inert as no producer at all (it reads nothing, or stale bytes), and
|
|
479
536
|
// only --final refuses that shape — a plain run reports every gate PASS.
|
|
480
|
-
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
|
+
}
|
|
481
624
|
const id = truncatedTo(oneLineOf(checkers[0].id), templateBudget(WHATS['gates-inert']));
|
|
482
625
|
// The fill can only help when it would land the producer in the right place: the checker must
|
|
483
626
|
// 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
|
|
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));
|
|
627
|
+
// actually carry a producer the fill would ACCEPT (computed above).
|
|
488
628
|
const checkerIsLast = checkers[0] === gates[gates.length - 1];
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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`;
|
|
501
641
|
add(
|
|
502
642
|
'gates-inert',
|
|
503
643
|
fillTemplate(WHATS['gates-inert'], { id }),
|
|
@@ -511,10 +651,6 @@ export const probeGatesInert = ({ root, deps, add, skip }) => {
|
|
|
511
651
|
// review-dependent (it needs no receipt), so the review-dependent predicate alone cannot see it.
|
|
512
652
|
// Left out, a matrix of nothing but that gate reads as carrying project verification — and a
|
|
513
653
|
// 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
654
|
if (gates.every((gate) => isKitOwnedCheckerGate(gate, root))) {
|
|
519
655
|
const declaredIds = new Set(gates.map((gate) => gate.id));
|
|
520
656
|
// Only PROJECT-verification entries: a non-colliding entry is not enough, it has to be one
|
|
@@ -541,31 +677,78 @@ export const probeGatesInert = ({ root, deps, add, skip }) => {
|
|
|
541
677
|
// the capability registry was built for. New and existing deployments meet it identically, because
|
|
542
678
|
// the advisor section is mandatory at every upgrade.
|
|
543
679
|
//
|
|
544
|
-
// The probe asks
|
|
545
|
-
// the
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
//
|
|
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.
|
|
550
690
|
const probeSourceSize = ({ root, deps, add, skip }) => {
|
|
551
691
|
try {
|
|
552
692
|
const declaration = loadDeclaration(root, deps);
|
|
553
693
|
const gates = declaration.outcome === 'loaded' ? declaration.gates : [];
|
|
554
|
-
const
|
|
555
|
-
|
|
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) {
|
|
556
703
|
// A DECLARED gate is not the same fact as a working one: the checker refuses on every config
|
|
557
704
|
// state but MINTED, so a gate declared over an absent or half-written record reds the matrix
|
|
558
705
|
// 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.
|
|
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.
|
|
560
708
|
const { state } = loadSourceSizeConfig(root, deps);
|
|
561
|
-
if (state
|
|
562
|
-
|
|
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
|
+
);
|
|
563
736
|
return;
|
|
564
737
|
}
|
|
565
|
-
// The
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
|
|
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);
|
|
569
752
|
} catch (err) {
|
|
570
753
|
skip('source-size', err);
|
|
571
754
|
}
|
|
@@ -627,15 +810,17 @@ const probeCommitGuard = ({ root, deps, add, skip }) => {
|
|
|
627
810
|
// or un-wired hook is covered there, never double-offered here. The apply is the gate-hook
|
|
628
811
|
// --read-lane PREVIEW one-liner (its own currency check + posture note fire at the writer; it may
|
|
629
812
|
// prompt once — it IS a consent flow). Converges once lanes.json enables the lane.
|
|
630
|
-
const probeReadLane = ({ root, deps, add, skip }) => {
|
|
813
|
+
const probeReadLane = ({ root, deps, add, skip, shared }) => {
|
|
631
814
|
try {
|
|
632
815
|
const sg = surveyGateHook(root, deps);
|
|
633
816
|
if (sg.error) throw new Error(sg.error);
|
|
634
817
|
if (!sg.wired) return; // not wired → the gate-hook item covers (no double-fire)
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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 = () =>
|
|
638
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();
|
|
639
824
|
return;
|
|
640
825
|
}
|
|
641
826
|
if (readReadLaneToggle(root, deps)) {
|
|
@@ -643,7 +828,23 @@ const probeReadLane = ({ root, deps, add, skip }) => {
|
|
|
643
828
|
// reads lanes.json, so the enabled lane is a silent no-op the user must reseed (council B7). The
|
|
644
829
|
// rm target is ABSOLUTE (council R2-M3) so running the recovery from any cwd can only delete this
|
|
645
830
|
// repo's hook.
|
|
646
|
-
|
|
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;
|
|
647
848
|
add(
|
|
648
849
|
'read-lane',
|
|
649
850
|
fillTemplate(WHATS['read-lane.stale'], {}),
|
|
@@ -825,6 +1026,13 @@ export const recipeFingerprint = ({ hosts, dirs, home }) => {
|
|
|
825
1026
|
return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
|
|
826
1027
|
};
|
|
827
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
|
+
|
|
828
1036
|
// The kit-owned neutral ack store (D4; AD-055 Part I): a FAMILY-OWNED strict-JSON file no host
|
|
829
1037
|
// validator guards — top-level key `sandboxLaneAck` (+ optional `_README`), unknown keys tolerated
|
|
830
1038
|
// on read (future acks are siblings). This is the PRIMARY ack channel; the legacy settings-scope
|
|
@@ -833,12 +1041,21 @@ export const recipeFingerprint = ({ hosts, dirs, home }) => {
|
|
|
833
1041
|
export const ACKS_FILE = 'docs/ai/acks.json';
|
|
834
1042
|
export const ACKS_LANE_KEY = 'sandboxLaneAck';
|
|
835
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';
|
|
836
1046
|
// The CLOSED-WORLD ack-lane registry: the lane name an advisor item renders on the writer's
|
|
837
1047
|
// command line → the store key that writer sets. A lane the registry does not name is a usage
|
|
838
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.
|
|
839
1054
|
export const ACK_LANES = Object.freeze({
|
|
840
1055
|
'sandbox-lane': ACKS_LANE_KEY,
|
|
841
1056
|
'worktrees-dir': ACKS_WORKTREES_DIR_KEY,
|
|
1057
|
+
'coverage-domain': ACKS_COVERAGE_DOMAIN_KEY,
|
|
1058
|
+
'source-size-copy': ACKS_SOURCE_SIZE_COPY_KEY,
|
|
842
1059
|
});
|
|
843
1060
|
|
|
844
1061
|
// The opt-in read-lane toggle file (AD-055 Part II) — the SAME kit-owned docs/ai/lanes.json the
|
|
@@ -875,13 +1092,33 @@ const isStateBlockGuardWired = (data) => {
|
|
|
875
1092
|
: runsStateBlockGuard(entry)));
|
|
876
1093
|
};
|
|
877
1094
|
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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));
|
|
885
1122
|
};
|
|
886
1123
|
|
|
887
1124
|
// The LEGACY neutral ack namespace (pre-AD-055): read from BOTH settings scopes until the next kit
|
|
@@ -951,7 +1188,7 @@ const readReadLaneToggle = (root, deps) => {
|
|
|
951
1188
|
// D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
|
|
952
1189
|
// at the consent moment; the static contract test asserts EXACT bidirectional coverage
|
|
953
1190
|
// (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
|
|
954
|
-
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']);
|
|
955
1192
|
|
|
956
1193
|
const probeSandboxLane = ({ root, deps, add, skip }) => {
|
|
957
1194
|
try {
|
|
@@ -1175,27 +1412,39 @@ export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
|
|
|
1175
1412
|
const skip = (key, err) => skips.push({ key, reason: truncatedTo(oneLineOf(err?.message ?? String(err)), SKIP_REASON_CAP) });
|
|
1176
1413
|
// The runtime shape backstop (D2): every COMPOSED item is validated at construction — a
|
|
1177
1414
|
// violation surfaces through the stated-skip lane, never a crash, never a rendered violation.
|
|
1178
|
-
//
|
|
1179
|
-
//
|
|
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.
|
|
1180
1420
|
// `detail` (optional) is an extra rendered `recipe:` line — factual context that is TOO LONG for
|
|
1181
1421
|
// the capped WHAT and does NOT belong in the pure-command apply (the sandbox-lane live recipe:
|
|
1182
1422
|
// egress hosts + resolved writable dirs; the worktrees-dir hand-apply-first grant advice; the
|
|
1183
1423
|
// agents hidden-mode reconcile follow-up). Single-line like apply; absent for every other item.
|
|
1184
|
-
|
|
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) => {
|
|
1185
1429
|
const problems = [];
|
|
1186
1430
|
if (!(key in BENEFITS)) problems.push(`unregistered item key ${JSON.stringify(key)}`);
|
|
1187
|
-
if (!(
|
|
1431
|
+
if (!(variant in SEVERITIES)) problems.push(`unregistered severity key ${JSON.stringify(variant)}`);
|
|
1188
1432
|
if (/[\r\n]/.test(what)) problems.push('WHAT is not a single line');
|
|
1189
1433
|
else if (what.length > ITEM_LINE_CAP) problems.push(`WHAT exceeds the ${ITEM_LINE_CAP}-char cap (${what.length})`);
|
|
1190
1434
|
if (/[\r\n]/.test(apply)) problems.push('apply is not a single line');
|
|
1191
1435
|
if (detail != null && /[\r\n]/.test(detail)) problems.push('recipe detail is not a single line');
|
|
1192
1436
|
if (problems.length > 0) {
|
|
1193
1437
|
skip(key, new Error(`item shape violation — ${problems.join('; ')}`));
|
|
1194
|
-
return;
|
|
1438
|
+
return false;
|
|
1195
1439
|
}
|
|
1196
|
-
items.push({ key, severity: SEVERITIES[
|
|
1440
|
+
items.push({ key, variant, severity: SEVERITIES[variant], what, benefit: BENEFITS[key], apply, detail });
|
|
1441
|
+
return true;
|
|
1197
1442
|
};
|
|
1198
|
-
|
|
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 });
|
|
1199
1448
|
return { root, items, skips };
|
|
1200
1449
|
};
|
|
1201
1450
|
|