@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
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// refresh-parity.mjs — the ONE bundle↔placed comparison walk, plus the POST-FAILURE parity reading
|
|
2
|
+
// of its result (feedback-hardening Plan 1 F3 / D3+D4).
|
|
3
|
+
//
|
|
4
|
+
// Two callers read the SAME walk for two different questions, so there is exactly one scanner:
|
|
5
|
+
// • the refresh itself asks "what did my overwrite replace?" — it scans BEFORE copying, and an
|
|
6
|
+
// absent placed file is a pure ADD (nothing local to lose), so it is not reported;
|
|
7
|
+
// • the read-only degrade asks "does the placed tree still MATCH the bundle?" — it scans AFTER the
|
|
8
|
+
// write was refused, and an absent placed file is exactly the drift a writable rerun repairs.
|
|
9
|
+
// The walk therefore reports FOUR buckets (drifted / unreadable / absent / conflicts) and each
|
|
10
|
+
// caller decides what they mean. A second scanner would be a second definition of "the same".
|
|
11
|
+
//
|
|
12
|
+
// Why the honesty bar (D4): the read-only skip line used to claim, unconditionally, that the tree
|
|
13
|
+
// "may be PARTIALLY updated" and that "any remaining drift persists" — two claims about post-state
|
|
14
|
+
// that nothing had checked. Every clause a line here composes binds to a state PROVEN this run: the
|
|
15
|
+
// verdict is computed from a real re-scan, and anything the re-scan could not read WITHHOLDS the calm
|
|
16
|
+
// claim (reported as could-not-verify) instead of being rendered as clean.
|
|
17
|
+
//
|
|
18
|
+
// The wrapper axis is part of the claim on purpose: the degrade returns before the caller's
|
|
19
|
+
// linkWrappers step, so a "nothing to repair" that only looked at files would be an unproven claim
|
|
20
|
+
// about a reconcile that never ran.
|
|
21
|
+
//
|
|
22
|
+
// Pure of process state and of the kit's own writers — a LEAF (fs is injected, nothing is imported
|
|
23
|
+
// from the caller). Read-only: never writes, never spawns. Dependency-free, Node >= 22.
|
|
24
|
+
|
|
25
|
+
import { join, resolve, relative, dirname, isAbsolute, sep } from 'node:path';
|
|
26
|
+
|
|
27
|
+
// The CLOSED parity verdict vocabulary — the three states the read-only skip line may report.
|
|
28
|
+
// doc-parity binds every value into the setup + upgrade mode contracts, so a doc that drops or
|
|
29
|
+
// renames an outcome fails a declared gate instead of describing a verdict the tool never emits.
|
|
30
|
+
export const PARITY = Object.freeze({
|
|
31
|
+
clean: 'clean-parity',
|
|
32
|
+
drifted: 'drifted',
|
|
33
|
+
unverifiable: 'unverifiable',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// The recovery every read-only outcome points at: the in-session `setup` would hit the same read-only
|
|
37
|
+
// dir, so the only real repair is a writable session. Shared by the skip line and the (unchanged)
|
|
38
|
+
// version-behind failure line, so the two can never drift apart.
|
|
39
|
+
export const READONLY_RERUN_HINT = 're-run the refresh from a writable session (e.g. outside the read-only sandbox)';
|
|
40
|
+
|
|
41
|
+
// The mode a managed wrapper source carries — the ONE definition, imported by the link step that
|
|
42
|
+
// chmods it and by the parity check that reads it back. "Executable enough" is not parity: a rerun
|
|
43
|
+
// sets exactly this, so any other mode is a state the rerun would change.
|
|
44
|
+
export const WRAPPER_MODE = 0o755;
|
|
45
|
+
|
|
46
|
+
// lstat NO-FOLLOW, classified: the Stats object | 'absent' (ENOENT) | 'error' (any other fs failure).
|
|
47
|
+
// Never reads THROUGH the node — a symlinked placed path is a "could not compare", never a read of
|
|
48
|
+
// whatever it points at.
|
|
49
|
+
const probe = (path, fs) => {
|
|
50
|
+
try {
|
|
51
|
+
return fs.lstat(path);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return err && err.code === 'ENOENT' ? 'absent' : 'error';
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// The reconcile-set walk: every bundle-owned node, classified by WHAT THE REFRESH COULD DO TO IT —
|
|
58
|
+
// not by whether this scanner could read it. The distinction is the whole point: "I could not compare
|
|
59
|
+
// this" and "the refresh cannot bring this to the required state" are different facts with different
|
|
60
|
+
// recoveries, and the earlier shape collapsed them.
|
|
61
|
+
//
|
|
62
|
+
// The writer's own policy decides the partition. copyTreeRefresh guards EVERY dest through
|
|
63
|
+
// assertContainedRealPath, whose walk includes the LEAF — so any placed symlink on a node in the
|
|
64
|
+
// reconcile-set is refused before the copy dispatch is even reached. Past that guard: a bundled
|
|
65
|
+
// symlink is ADDITIVE (created when the dest is absent, otherwise left alone — an explicit no-op, not
|
|
66
|
+
// a comparison), a bundled directory is mkdir -p (EEXIST over a file), a bundled file is copyFile
|
|
67
|
+
// (EISDIR over a directory; a device or FIFO may be WRITTEN INTO or block rather than be replaced).
|
|
68
|
+
// The BUNDLE read is our own shipped artifact — a failure there is a loud corrupt-kit error upstream
|
|
69
|
+
// (never swallowed here); only the PLACED read is caught. Buckets, each sorted:
|
|
70
|
+
// drifted — placed bytes differ from the bundle; a rerun overwrites them
|
|
71
|
+
// absent — the bundle ships it and the placed tree does not; a rerun creates it
|
|
72
|
+
// conflicts — a rerun cannot be GUARANTEED to converge this node under the writer's no-follow /
|
|
73
|
+
// ownership policy (any placed symlink; an incompatible shape). Labeled with the cause
|
|
74
|
+
// unreadable — a genuine read/stat error, and nothing else
|
|
75
|
+
// Plus `modes`: the placed mode of every node the walk actually reached as a regular file. It exists
|
|
76
|
+
// so the wrapper axis can judge a source's mode WITHOUT a second lstat of its own — a separate stat
|
|
77
|
+
// could traverse a symlinked ancestor this walk already refused, and produce a contradicting verdict.
|
|
78
|
+
export const scanBundleOwnedDrift = (bundleDir, skillDir, fs) => {
|
|
79
|
+
const drifted = [];
|
|
80
|
+
const unreadable = [];
|
|
81
|
+
const absent = [];
|
|
82
|
+
const conflicts = [];
|
|
83
|
+
const modes = new Map();
|
|
84
|
+
const refuse = (rel, cause) => conflicts.push(`${rel} (${cause})`);
|
|
85
|
+
const walk = (rel) => {
|
|
86
|
+
const src = join(bundleDir, rel);
|
|
87
|
+
const dest = join(skillDir, rel);
|
|
88
|
+
const st = fs.lstat(src);
|
|
89
|
+
const placed = rel === '' ? null : probe(dest, fs);
|
|
90
|
+
// The skill dir itself (rel '') is the caller's own inspected root — it proved that node is a real
|
|
91
|
+
// directory before any of this ran. Every node BELOW it meets the containment guard first, and the
|
|
92
|
+
// guard refuses a symlink at the leaf just as it does at an ancestor.
|
|
93
|
+
if (placed === 'error') {
|
|
94
|
+
unreadable.push(rel);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (placed !== null && placed !== 'absent' && placed.isSymbolicLink()) {
|
|
98
|
+
refuse(rel, 'a symlink is in the way');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (st.isSymbolicLink()) {
|
|
102
|
+
// ADDITIVE by contract: created when absent, otherwise left alone WITHOUT comparison. A node
|
|
103
|
+
// left alone is outside every claim this scan makes — it is neither drift nor a finding.
|
|
104
|
+
if (placed === 'absent') absent.push(rel);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (st.isDirectory()) {
|
|
108
|
+
if (placed !== null && placed !== 'absent' && !placed.isDirectory()) {
|
|
109
|
+
refuse(rel, 'a non-directory is in the way');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const entries = fs.readdir(src);
|
|
113
|
+
// An EMPTY bundled dir names nothing below it, so its absence would go unreported — record the
|
|
114
|
+
// dir itself. A NON-empty one is already named by its children; recording it too would
|
|
115
|
+
// double-report one absence.
|
|
116
|
+
if (placed === 'absent' && entries.length === 0) absent.push(rel);
|
|
117
|
+
for (const entry of entries) walk(rel ? join(rel, entry) : entry);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (placed === 'absent') {
|
|
121
|
+
absent.push(rel);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!placed.isFile()) {
|
|
125
|
+
refuse(rel, 'a node of the wrong kind is in the way');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (typeof placed.mode === 'number') modes.set(rel, placed.mode);
|
|
129
|
+
const srcBytes = fs.readFile(src);
|
|
130
|
+
const destBytes = (() => {
|
|
131
|
+
try {
|
|
132
|
+
return fs.readFile(dest);
|
|
133
|
+
} catch {
|
|
134
|
+
unreadable.push(rel);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
})();
|
|
138
|
+
if (destBytes === null) return;
|
|
139
|
+
if (!Buffer.from(srcBytes).equals(Buffer.from(destBytes))) drifted.push(rel);
|
|
140
|
+
};
|
|
141
|
+
walk('');
|
|
142
|
+
return {
|
|
143
|
+
drifted: drifted.sort(), unreadable: unreadable.sort(), absent: absent.sort(), conflicts: conflicts.sort(), modes,
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// A wrapper has TWO axes the link step touches — the LINK, which lives outside the bundle tree, and
|
|
148
|
+
// the SOURCE, which does not. Each outcome is `null` (nothing to say) or a `[bucketName, cause]`
|
|
149
|
+
// tuple the caller files under that bucket, labeled by axis: a wrapper broken on one axis and
|
|
150
|
+
// unknown on the other rides BOTH lists rather than losing a fact the re-scan proved.
|
|
151
|
+
const WRAPPER_CLEAN = null;
|
|
152
|
+
|
|
153
|
+
// The link: absent is repairable (the link step creates it); anything else standing there is a
|
|
154
|
+
// refusal (linkManaged replaces ONLY a symlink already pointing at our source). Never follows a
|
|
155
|
+
// foreign link — readlink, then a string compare against the SAME physical base the writer uses:
|
|
156
|
+
// linkWrappers canonicalises the bindir through realpath before linking, so a relative target read
|
|
157
|
+
// from a symlinked bindir must resolve against the real directory or the two would disagree. A
|
|
158
|
+
// realpath failure is could-not-verify with NO lexical fallback — falling back could call a foreign
|
|
159
|
+
// link ours.
|
|
160
|
+
const wrapperDstOutcome = (link, fs) => {
|
|
161
|
+
const dstStat = probe(link.dst, fs);
|
|
162
|
+
if (dstStat === 'error') return ['unverifiable', 'its link could not be read'];
|
|
163
|
+
if (dstStat === 'absent') return ['drifted', 'not linked'];
|
|
164
|
+
if (!dstStat.isSymbolicLink()) return ['conflicts', 'a non-symlink is in the way'];
|
|
165
|
+
let target;
|
|
166
|
+
try {
|
|
167
|
+
target = fs.readlink(link.dst);
|
|
168
|
+
} catch {
|
|
169
|
+
return ['unverifiable', 'its link target could not be read'];
|
|
170
|
+
}
|
|
171
|
+
let resolved;
|
|
172
|
+
if (isAbsolute(target)) resolved = target;
|
|
173
|
+
else {
|
|
174
|
+
try {
|
|
175
|
+
resolved = resolve(fs.realpath(dirname(link.dst)), target);
|
|
176
|
+
} catch {
|
|
177
|
+
return ['unverifiable', 'its link directory could not be resolved'];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return resolved === resolve(link.source) ? WRAPPER_CLEAN : ['conflicts', 'a foreign symlink is in the way'];
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// The source is a BUNDLE-OWNED file (deriveLinks resolves it inside the skill dir and planFor proves
|
|
184
|
+
// the bundle ships it), so the reconcile-set walk already classified its existence and its shape and
|
|
185
|
+
// already named it if anything was wrong. This axis therefore owns exactly ONE fact the walk does not
|
|
186
|
+
// compare: the mode. A node the walk never reached — absent, refused, or unreadable — yields nothing
|
|
187
|
+
// here, so one broken file is never reported twice under two different names.
|
|
188
|
+
const wrapperSourceOutcome = (link, skillDir, modes) => {
|
|
189
|
+
const rel = relative(skillDir, link.source).split(sep).join('/');
|
|
190
|
+
if (!modes.has(rel)) return WRAPPER_CLEAN;
|
|
191
|
+
return (modes.get(rel) & 0o7777) === WRAPPER_MODE ? WRAPPER_CLEAN : ['drifted', 'its source mode differs'];
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const scanWrapperParity = (links, fs, { skillDir, modes }) => {
|
|
195
|
+
const out = { drifted: [], conflicts: [], unverifiable: [] };
|
|
196
|
+
for (const link of links ?? []) {
|
|
197
|
+
for (const outcome of [wrapperDstOutcome(link, fs), wrapperSourceOutcome(link, skillDir, modes)]) {
|
|
198
|
+
if (outcome === WRAPPER_CLEAN) continue;
|
|
199
|
+
const [bucket, cause] = outcome;
|
|
200
|
+
out[bucket].push(`wrapper ${link.cmd} (${cause})`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { drifted: out.drifted.sort(), conflicts: out.conflicts.sort(), unverifiable: out.unverifiable.sort() };
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// The ONE verdict the line is composed from. `clean-parity` requires EVERY list empty — any node the
|
|
207
|
+
// re-scan could not compare withholds the calm claim rather than being counted as equal. `drifted` is
|
|
208
|
+
// the headline whenever something is provably wrong (repairable or refused): a proven break outranks
|
|
209
|
+
// an unknown, and the line still names every list, so no proven fact is collapsed into another.
|
|
210
|
+
export const parityVerdict = ({ scan, wrappers }) => {
|
|
211
|
+
const drifted = [...scan.drifted, ...scan.absent, ...wrappers.drifted].sort();
|
|
212
|
+
const conflicts = [...scan.conflicts, ...wrappers.conflicts].sort();
|
|
213
|
+
const unverifiable = [...scan.unreadable, ...wrappers.unverifiable].sort();
|
|
214
|
+
const broken = drifted.length > 0 || conflicts.length > 0;
|
|
215
|
+
const state = broken ? PARITY.drifted : unverifiable.length > 0 ? PARITY.unverifiable : PARITY.clean;
|
|
216
|
+
return { state, drifted, conflicts, unverifiable };
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// The verdict a caller reports when the re-scan itself could not run (a corrupt/unreadable bundle,
|
|
220
|
+
// any fs failure raised out of the walk). Could-not-verify is the honest floor — never a false clean.
|
|
221
|
+
export const unverifiableParity = (item) => ({ state: PARITY.unverifiable, drifted: [], conflicts: [], unverifiable: [item] });
|
|
222
|
+
|
|
223
|
+
const count = (items) => `${items.length} item(s)`;
|
|
224
|
+
|
|
225
|
+
// The read-only skip line (outcome `skipped-readonly`, exit 0 — the stated skip). Two clauses are
|
|
226
|
+
// proven by the caller before it composes: the versions are KNOWN equal, and the failure was tagged
|
|
227
|
+
// at a WRITE boundary. Every remaining clause comes from the verdict — no clause without its check,
|
|
228
|
+
// and every item the verdict carries is NAMED (a count alone would not tell the user what to look at).
|
|
229
|
+
export const readonlySkipLine = (name, version, verdict) => {
|
|
230
|
+
const head = ` ${name}: already current${version ? ` (v${version})` : ''} — the file re-sync could not write: ` +
|
|
231
|
+
'the skills directory is read-only this session.';
|
|
232
|
+
if (verdict.state === PARITY.clean) {
|
|
233
|
+
// The claim is scoped to what the refresh MANAGES, and it names each checked axis rather than
|
|
234
|
+
// generalising: a rerun would still rewrite byte-equal files (so "nothing would change" would be
|
|
235
|
+
// false), a placed-only extra is preserved and never in scope, and an additive node the writer
|
|
236
|
+
// leaves alone is outside the comparison by contract.
|
|
237
|
+
return `${head} A read-only re-scan found no refresh-managed difference to repair: every file the ` +
|
|
238
|
+
'refresh would overwrite already matches, every node it would add is present, and every wrapper ' +
|
|
239
|
+
'link and source mode is in place.';
|
|
240
|
+
}
|
|
241
|
+
// One sentence per non-empty list, each carrying the recovery that actually applies to it: the
|
|
242
|
+
// writable rerun REPAIRS the drifted set, and REFUSES the conflicting set — one blanket "re-run to
|
|
243
|
+
// repair" over both would promise a repair that never happens.
|
|
244
|
+
const said = [];
|
|
245
|
+
if (verdict.drifted.length > 0) {
|
|
246
|
+
said.push(`A read-only re-scan found ${count(verdict.drifted)} still differing from the bundled copy: ` +
|
|
247
|
+
`${verdict.drifted.join(', ')} — ${READONLY_RERUN_HINT} to repair.`);
|
|
248
|
+
}
|
|
249
|
+
if (verdict.conflicts.length > 0) {
|
|
250
|
+
said.push(`${said.length > 0 ? 'It also found' : 'A read-only re-scan found'} ${count(verdict.conflicts)} a ` +
|
|
251
|
+
`writable rerun would REFUSE rather than repair: ${verdict.conflicts.join(', ')} — resolve each by hand, ` +
|
|
252
|
+
'then re-run the refresh.');
|
|
253
|
+
}
|
|
254
|
+
if (verdict.unverifiable.length > 0) {
|
|
255
|
+
// The unknown-need clause and its recovery ride EVERY unverifiable outcome, not only the one
|
|
256
|
+
// that happens to open the line: a reader who already saw a drift sentence would otherwise be
|
|
257
|
+
// left with a list of names and no statement of what is unknown about them or what to do.
|
|
258
|
+
const lead = said.length > 0 ? 'It could not verify a further' : 'A read-only re-scan could NOT verify';
|
|
259
|
+
said.push(`${lead} ${count(verdict.unverifiable)}: ${verdict.unverifiable.join(', ')} — whether those ` +
|
|
260
|
+
`still need repair is unknown; ${READONLY_RERUN_HINT}.`);
|
|
261
|
+
}
|
|
262
|
+
return `${head} ${said.join(' ')}`;
|
|
263
|
+
};
|
package/tools/run-gates.mjs
CHANGED
|
@@ -140,7 +140,7 @@ const USAGE = [
|
|
|
140
140
|
'exported to every gate child, and STRIPPED from the inherited environment first — a host-set',
|
|
141
141
|
'value never stands in for a computed one. A selected gate referencing a producer variable this',
|
|
142
142
|
'run will not set is refused BEFORE anything spawns (exit 1), naming the gate, the variable, and',
|
|
143
|
-
'the remedy — never left to
|
|
143
|
+
'the remedy — never left to run without the runner-produced value.',
|
|
144
144
|
'Sandbox-safe: the runner itself needs no network and writes only repo-local state — the D4 sandbox',
|
|
145
145
|
'lane; each DECLARED gate command is the project\'s own, so ITS sandbox-safety is command-shape',
|
|
146
146
|
'dependent (first try the sandbox-safe shape — cache under $TMPDIR, offline/notifier off).',
|
|
@@ -197,9 +197,12 @@ export const spawnGateViaBash = (cmd, cwd, extraEnv = {}) => {
|
|
|
197
197
|
return spawnSync('bash', ['-c', cmd], { cwd, env: { ...env, ...extraEnv }, encoding: 'utf8', maxBuffer: MAX_GATE_OUTPUT_BYTES });
|
|
198
198
|
};
|
|
199
199
|
|
|
200
|
-
// A `$VAR` / `${VAR}` reference to a producer variable.
|
|
201
|
-
//
|
|
202
|
-
|
|
200
|
+
// A `$VAR` / `${VAR}` / `${VAR:?word}` reference to a producer variable. The required-parameter form
|
|
201
|
+
// is the canonical producer destination: it makes an uninjected expansion fail BY NAME instead of
|
|
202
|
+
// writing under an empty prefix, and it still depends on the injection — so it must refuse here,
|
|
203
|
+
// pre-spawn, rather than dying inside a gate child far from its cause. A `${VAR:-default}` form
|
|
204
|
+
// stays deliberately unmatched — a cmd carrying its own fallback does not depend on the injection.
|
|
205
|
+
const referencesProducer = (cmd, name) => new RegExp(`\\$\\{${name}(?:\\}|:\\?)|\\$${name}(?![A-Za-z0-9_])`).test(cmd);
|
|
203
206
|
|
|
204
207
|
const PRODUCER_RECOVERY = Object.freeze({
|
|
205
208
|
AW_GIT_DIR: 'run from inside a git work tree — the runner resolves the git dir there and exports it to every gate.',
|
|
@@ -609,7 +612,7 @@ export const runCli = (argv, deps = {}) => {
|
|
|
609
612
|
const unmet = findUnmetProducerRefs(selected, injectedEnv);
|
|
610
613
|
if (unmet.length > 0) {
|
|
611
614
|
for (const { id, name } of unmet) {
|
|
612
|
-
logError(`[run-gates] gate "${id}" references $${name}, which this run will not set —
|
|
615
|
+
logError(`[run-gates] gate "${id}" references $${name}, which this run will not set — the child would run without the runner-produced value.`);
|
|
613
616
|
logError(` Recovery: ${PRODUCER_RECOVERY[name]}`);
|
|
614
617
|
}
|
|
615
618
|
releaseSubsetRunLock();
|
package/tools/setup-backends.mjs
CHANGED
|
@@ -32,6 +32,10 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
32
32
|
import os from 'node:os';
|
|
33
33
|
import { KNOWN_BACKENDS, detectBackend, detectBackends, resolveDir, guideFor, READY } from './detect-backends.mjs';
|
|
34
34
|
import { copyTreeRefresh, linkManaged, isReadonlyWriteBoundary } from './fs-safe.mjs';
|
|
35
|
+
import {
|
|
36
|
+
READONLY_RERUN_HINT, WRAPPER_MODE, scanBundleOwnedDrift, scanWrapperParity, parityVerdict,
|
|
37
|
+
unverifiableParity, readonlySkipLine,
|
|
38
|
+
} from './refresh-parity.mjs';
|
|
35
39
|
import { validateManifest, readAuthoritativeVersion, UNSUPPORTED, INVALID } from './manifest/validate.mjs';
|
|
36
40
|
import { compareSemver } from './semver-lite.mjs';
|
|
37
41
|
|
|
@@ -113,62 +117,16 @@ const probeMarker = (file, fs) => {
|
|
|
113
117
|
const SETTINGS_FILE_HINT = '${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf';
|
|
114
118
|
const SETTINGS_CMD_HINT = '/agent-workflow-kit bridge-settings';
|
|
115
119
|
|
|
116
|
-
//
|
|
117
|
-
// refresh
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
// PLACED read is caught (EACCES/EIO → 'unreadable', and the refresh still proceeds). Sorted output is
|
|
123
|
-
// deterministic for the stated line. Exported for a direct unit test (the full driver cannot observe a
|
|
124
|
-
// symlinked placed file — copyTreeRefresh refuses to overwrite one, so the refresh fails before any line).
|
|
125
|
-
export const scanBundleOwnedDrift = (bundleDir, skillDir, fs) => {
|
|
126
|
-
const drifted = [];
|
|
127
|
-
const unreadable = [];
|
|
128
|
-
const walk = (rel) => {
|
|
129
|
-
const src = join(bundleDir, rel);
|
|
130
|
-
const dest = join(skillDir, rel);
|
|
131
|
-
const st = fs.lstat(src);
|
|
132
|
-
if (st.isSymbolicLink()) return;
|
|
133
|
-
if (st.isDirectory()) {
|
|
134
|
-
for (const entry of fs.readdir(src)) walk(rel ? join(rel, entry) : entry);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
// lstat the PLACED path NO-FOLLOW first — never read THROUGH a symlink (copyTreeRefresh's
|
|
138
|
-
// assertContainedRealPath would refuse to overwrite a symlinked dest, so reading its target here
|
|
139
|
-
// would be both unsafe and moot). Absent → a bundled-only addition (no local loss); a symlink /
|
|
140
|
-
// non-regular / unreadable placed node → "could not compare" without a read-through.
|
|
141
|
-
const destStat = (() => {
|
|
142
|
-
try {
|
|
143
|
-
return fs.lstat(dest);
|
|
144
|
-
} catch (err) {
|
|
145
|
-
return err && err.code === 'ENOENT' ? 'absent' : 'error';
|
|
146
|
-
}
|
|
147
|
-
})();
|
|
148
|
-
if (destStat === 'absent') return;
|
|
149
|
-
if (destStat === 'error' || destStat.isSymbolicLink() || !destStat.isFile()) {
|
|
150
|
-
unreadable.push(rel);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
const srcBytes = fs.readFile(src);
|
|
154
|
-
const destBytes = (() => {
|
|
155
|
-
try {
|
|
156
|
-
return fs.readFile(dest);
|
|
157
|
-
} catch {
|
|
158
|
-
unreadable.push(rel);
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
})();
|
|
162
|
-
if (destBytes === null) return;
|
|
163
|
-
if (!Buffer.from(srcBytes).equals(Buffer.from(destBytes))) drifted.push(rel);
|
|
164
|
-
};
|
|
165
|
-
walk('');
|
|
166
|
-
return { drifted: drifted.sort(), unreadable: unreadable.sort() };
|
|
167
|
-
};
|
|
120
|
+
// The bundle↔placed comparison walk lives in refresh-parity.mjs — ONE scanner, two readings (this
|
|
121
|
+
// module's refresh-time drift report below, and the read-only degrade's post-failure parity verdict).
|
|
122
|
+
// Re-exported so the scan's own unit tests and any consumer keep importing it from the writer they
|
|
123
|
+
// already know. In THIS reading a bundled-only file (the scan's `absent` bucket) is a pure add — no
|
|
124
|
+
// local loss — so `driftSummary` ignores it; the parity reading is the one that calls it drift.
|
|
125
|
+
export { scanBundleOwnedDrift };
|
|
168
126
|
|
|
169
127
|
// One user-facing sentence naming what an equal-version re-sync overwrote — or null when nothing local
|
|
170
128
|
// was lost. Callers apply their own indent; the pointer names the settings file that survives a refresh.
|
|
171
|
-
const driftSummary = (drift) => {
|
|
129
|
+
export const driftSummary = (drift) => {
|
|
172
130
|
if (!drift) return null;
|
|
173
131
|
const parts = [];
|
|
174
132
|
if (drift.drifted.length) parts.push(`overwrote ${drift.drifted.length} locally-changed file(s): ${drift.drifted.join(', ')}`);
|
|
@@ -284,6 +242,10 @@ export const deriveLinks = (manifest, skillDir) => {
|
|
|
284
242
|
};
|
|
285
243
|
|
|
286
244
|
// Classify a wrapper dst per-bindir (NOT PATH-wide): absent | ours (symlink → our source) | conflict.
|
|
245
|
+
// A RELATIVE target resolves against the dst's PHYSICAL parent, because linkWrappers canonicalises the
|
|
246
|
+
// bindir through realpath before it links: under a symlinked bindir (a common dotfiles setup) a
|
|
247
|
+
// lexical base would answer a different question than the one the writer asks. A realpath failure is
|
|
248
|
+
// a conflict, never a lexical fallback — the fallback could call a foreign link ours.
|
|
287
249
|
const inspectDst = (dst, source, fs) => {
|
|
288
250
|
const st = lstatNoFollow(dst, fs.lstat);
|
|
289
251
|
if (st === null) return { state: 'absent' };
|
|
@@ -294,7 +256,15 @@ const inspectDst = (dst, source, fs) => {
|
|
|
294
256
|
} catch (err) {
|
|
295
257
|
return { state: 'conflict', reason: `unreadable symlink (${err.code ?? 'fs error'})` };
|
|
296
258
|
}
|
|
297
|
-
|
|
259
|
+
let resolved;
|
|
260
|
+
if (isAbsolute(target)) resolved = target;
|
|
261
|
+
else {
|
|
262
|
+
try {
|
|
263
|
+
resolved = resolve(fs.realpath(dirname(dst)), target);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
return { state: 'conflict', reason: `cannot resolve the link's directory (${err.code ?? 'fs error'})` };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
298
268
|
return resolved === resolve(source) ? { state: 'ours' } : { state: 'conflict', reason: `foreign symlink → ${target}` };
|
|
299
269
|
};
|
|
300
270
|
|
|
@@ -378,7 +348,7 @@ export const linkWrappers = (skillDir, manifest, opts = {}) => {
|
|
|
378
348
|
const realBindir = fs.realpath(bindir);
|
|
379
349
|
const links = [];
|
|
380
350
|
for (const { cmd, source } of derived) {
|
|
381
|
-
fs.chmod(source,
|
|
351
|
+
fs.chmod(source, WRAPPER_MODE);
|
|
382
352
|
const action = linkManaged(source, join(realBindir, cmd), realBindir, fs); // 'linked' | 'noop'
|
|
383
353
|
links.push({ cmd, source, dst: join(bindir, cmd), action });
|
|
384
354
|
}
|
|
@@ -586,19 +556,56 @@ const refreshSkillOnly = (entry, deps = {}) => {
|
|
|
586
556
|
const NOT_PLACED_LINE = 'skipped — not placed (placement is opt-in: /agent-workflow-kit setup)';
|
|
587
557
|
const stripPrefix = (message) => message.replace('[agent-workflow-kit] ', '');
|
|
588
558
|
|
|
589
|
-
// The read-only degrade wording (D1). The STATED-skip line
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
|
|
595
|
-
const skippedReadonlyLine = (name, version) =>
|
|
596
|
-
` ${name}: already current${version ? ` (v${version})` : ''} — the re-sync was skipped/incomplete: ` +
|
|
597
|
-
`the skills directory is read-only this session (the tree may be PARTIALLY updated). Repair-on-rerun cannot ` +
|
|
598
|
-
`run here; any remaining drift persists until you ${READONLY_RERUN_HINT}.`;
|
|
559
|
+
// The read-only degrade wording (D1, honesty-split per D4). The STATED-skip line is composed in
|
|
560
|
+
// refresh-parity.mjs from a POST-FAILURE re-scan: the old line asserted "PARTIALLY updated" and "any
|
|
561
|
+
// remaining drift persists" unconditionally — two claims about post-state that nothing had checked.
|
|
562
|
+
// It still never claims a re-sync RAN (that is already-current's line). The FAILED line (a
|
|
563
|
+
// version-behind upgrade blocked by the same read-only dir) stays loud, but its recovery points at a
|
|
564
|
+
// writable rerun — the in-session `setup` would hit the same read-only dir.
|
|
599
565
|
const readonlyRefreshFailedLine = (name, message) =>
|
|
600
566
|
` ${name}: could not refresh — ${stripPrefix(message)}; the skills directory is read-only this session — ${READONLY_RERUN_HINT}`;
|
|
601
567
|
|
|
568
|
+
// The per-bridge refresh outcome lines, one pure composer per outcome — the composed-lines guard
|
|
569
|
+
// (test/composed-lines-ux.test.mjs) renders every key against the L2 user-grade invariants. `label`
|
|
570
|
+
// is the pre-rendered version suffix (versionLabel); reasons arrive already prefix-stripped.
|
|
571
|
+
export const REFRESH_LINES = Object.freeze({
|
|
572
|
+
unsupported: (name, reason) => ` ${name}: skipped — ${reason}`,
|
|
573
|
+
'kept-newer': (name, reason) => ` ${name}: skipped — ${reason}`,
|
|
574
|
+
'not-placed': (name) => ` ${name}: ${NOT_PLACED_LINE}`,
|
|
575
|
+
failed: (name, reason) => ` ${name}: could not refresh — ${reason}; recover with /agent-workflow-kit setup`,
|
|
576
|
+
'failed-readonly': readonlyRefreshFailedLine,
|
|
577
|
+
[SKIPPED_READONLY]: readonlySkipLine,
|
|
578
|
+
'already-current': (name, label, summary) => {
|
|
579
|
+
const base = ` ${name}: already current${label} — files re-synced from the bundled copy`;
|
|
580
|
+
return summary ? `${base}\n ↳ ${summary}` : base;
|
|
581
|
+
},
|
|
582
|
+
refreshed: (name, label) => ` ${name}: refreshed${label}`,
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// The post-failure parity verdict behind that line (D3): a read-only re-scan of the bundle-owned
|
|
586
|
+
// files AND the wrapper links, run AFTER the write refusal, so every clause binds to state proven
|
|
587
|
+
// THIS run. The degrade returns before linkWrappers, so the wrapper axis is genuinely unreconciled —
|
|
588
|
+
// a calm claim that skipped it would be unproven. Any failure raised out of the walk itself (the
|
|
589
|
+
// bundle read is deliberately uncaught in there) collapses to could-not-verify: the calm claim is
|
|
590
|
+
// withheld, never inverted into a false clean, and a re-scan problem never turns the stated skip
|
|
591
|
+
// into a reported failure.
|
|
592
|
+
const readonlyParity = (plan, deps) => {
|
|
593
|
+
const fs = fsDeps(deps);
|
|
594
|
+
try {
|
|
595
|
+
// ONE walk feeds both halves: the wrapper axis judges a source's mode from what the walk already
|
|
596
|
+
// reached, never from a second stat that could traverse an ancestor the walk refused.
|
|
597
|
+
const scan = scanBundleOwnedDrift(plan.place.bundleDir, plan.skillDir, fs);
|
|
598
|
+
return parityVerdict({
|
|
599
|
+
scan,
|
|
600
|
+
wrappers: scanWrapperParity(plan.links, fs, { skillDir: plan.skillDir, modes: scan.modes }),
|
|
601
|
+
});
|
|
602
|
+
} catch (err) {
|
|
603
|
+
// The catch cannot know WHICH side raised (the walk reads the bundle and the placed tree), so it
|
|
604
|
+
// names the comparison, never a side — attributing it would be the same unproven claim one layer down.
|
|
605
|
+
return unverifiableParity(`the bundle/placed comparison (${err.code ?? 'fs error'})`);
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
|
|
602
609
|
// Refresh every ALREADY-PLACED bridge from the kit's bundled copies and re-link its wrappers (a newer
|
|
603
610
|
// bridge can add one). One reported outcome per backend — never a crash; a per-backend STOP/error
|
|
604
611
|
// becomes a `failed` result. `line` is the tool-composed sentence callers print VERBATIM (the agent
|
|
@@ -612,55 +619,59 @@ export const refreshPlacedBridges = (deps = {}, names = KNOWN_BACKENDS.map((b) =
|
|
|
612
619
|
try {
|
|
613
620
|
const plan = planFor(name, deps);
|
|
614
621
|
if (plan.outcome === 'unsupported') {
|
|
615
|
-
return { name: plan.name, outcome: 'unsupported', line:
|
|
622
|
+
return { name: plan.name, outcome: 'unsupported', line: REFRESH_LINES.unsupported(plan.name, plan.reason) };
|
|
616
623
|
}
|
|
617
624
|
if (plan.wouldDowngrade) {
|
|
618
|
-
return { name: plan.name, outcome: 'kept-newer', line:
|
|
625
|
+
return { name: plan.name, outcome: 'kept-newer', line: REFRESH_LINES['kept-newer'](plan.name, stripPrefix(plan.reason)) };
|
|
619
626
|
}
|
|
620
627
|
// An absent/empty skill dir is `not placed` REGARDLESS of any later plan trouble (a foreign
|
|
621
628
|
// wrapper conflict, a bundle-source problem): the refresh-only driver skips an unplaced bridge
|
|
622
629
|
// before those axes matter, so it must never claim "could not refresh" what it would not touch.
|
|
623
630
|
if (plan.place && plan.place.action !== 'refresh') {
|
|
624
|
-
return { name: plan.name, outcome: 'not-placed', line:
|
|
631
|
+
return { name: plan.name, outcome: 'not-placed', line: REFRESH_LINES['not-placed'](plan.name) };
|
|
625
632
|
}
|
|
626
633
|
if (plan.outcome === 'stop' || plan.outcome === 'error') {
|
|
627
|
-
return { name: plan.name, outcome: 'failed', line:
|
|
634
|
+
return { name: plan.name, outcome: 'failed', line: REFRESH_LINES.failed(plan.name, stripPrefix(plan.reason)) };
|
|
628
635
|
}
|
|
629
636
|
const refresh = refreshSkillOnly(registryEntry(plan.name), deps);
|
|
630
637
|
// A read-only skills dir at an EQUAL version is a STATED skip (repair-on-rerun cannot run here) —
|
|
631
638
|
// never a false "could not refresh" and never a failure exit (D1/AD-056).
|
|
632
639
|
if (refresh.skippedReadonly) {
|
|
633
|
-
return {
|
|
640
|
+
return {
|
|
641
|
+
name: plan.name,
|
|
642
|
+
outcome: SKIPPED_READONLY,
|
|
643
|
+
line: REFRESH_LINES[SKIPPED_READONLY](plan.name, refresh.version, readonlyParity(plan, deps)),
|
|
644
|
+
};
|
|
634
645
|
}
|
|
635
646
|
if (!refresh.refreshed) {
|
|
636
|
-
return { name: plan.name, outcome: 'not-placed', line:
|
|
647
|
+
return { name: plan.name, outcome: 'not-placed', line: REFRESH_LINES['not-placed'](plan.name) };
|
|
637
648
|
}
|
|
638
649
|
const manifest = readBundledManifest(plan.place.bundleDir, deps);
|
|
639
650
|
linkWrappers(plan.skillDir, manifest, { ...deps, bindir: plan.bindir, platform: plan.platform });
|
|
640
651
|
const current = plan.version !== null && plan.priorVersion === plan.version;
|
|
641
652
|
// The equal-version line still states the copy that ran (repair-on-rerun) — the tool never
|
|
642
653
|
// reports a mutation-free "already current" while it re-synced files underneath.
|
|
643
|
-
const base = ` ${plan.name}: ${current ? `already current${versionLabel(plan)} — files re-synced from the bundled copy` : `refreshed${versionLabel(plan)}`}`;
|
|
644
654
|
// Overwrite honesty (D5): only an EQUAL-version re-sync can prove a byte diff is a LOCAL edit —
|
|
645
655
|
// a version upgrade's diffs are the version delta, which the (vOld → vNew) arrow already states.
|
|
646
|
-
const summary = current ? driftSummary(refresh.drift) : null;
|
|
647
656
|
return {
|
|
648
657
|
name: plan.name,
|
|
649
658
|
outcome: current ? 'already-current' : 'refreshed',
|
|
650
|
-
line:
|
|
659
|
+
line: current
|
|
660
|
+
? REFRESH_LINES['already-current'](plan.name, versionLabel(plan), driftSummary(refresh.drift))
|
|
661
|
+
: REFRESH_LINES.refreshed(plan.name, versionLabel(plan)),
|
|
651
662
|
};
|
|
652
663
|
} catch (err) {
|
|
653
664
|
// A downgrade STOP raised at the write boundary (a newer bridge landed between plan and apply)
|
|
654
665
|
// is the same stated skip as the planned one — classified structurally via the typed field.
|
|
655
666
|
if (err.wouldDowngrade) {
|
|
656
|
-
return { name: canonical, outcome: 'kept-newer', line:
|
|
667
|
+
return { name: canonical, outcome: 'kept-newer', line: REFRESH_LINES['kept-newer'](canonical, stripPrefix(err.message)) };
|
|
657
668
|
}
|
|
658
669
|
// A read-only WRITE failure on a version-BEHIND (upgrade) refresh stays a loud failure — but the
|
|
659
670
|
// in-session "recover with setup" would hit the same read-only dir, so point at a writable rerun.
|
|
660
671
|
if (isReadonlyWriteBoundary(err)) {
|
|
661
|
-
return { name: canonical, outcome: 'failed', line:
|
|
672
|
+
return { name: canonical, outcome: 'failed', line: REFRESH_LINES['failed-readonly'](canonical, err.message) };
|
|
662
673
|
}
|
|
663
|
-
return { name: canonical, outcome: 'failed', line:
|
|
674
|
+
return { name: canonical, outcome: 'failed', line: REFRESH_LINES.failed(canonical, stripPrefix(err.message)) };
|
|
664
675
|
}
|
|
665
676
|
});
|
|
666
677
|
|
|
@@ -26,10 +26,9 @@
|
|
|
26
26
|
// Exit codes: 0 green / 1 violation or refusal / 2 usage, config or enumeration error.
|
|
27
27
|
// Dependency-free, Node >= 22. No side effects on import.
|
|
28
28
|
|
|
29
|
-
import { fileURLToPath } from 'node:url';
|
|
30
|
-
import { realpathSync } from 'node:fs';
|
|
31
29
|
import { resolve } from 'node:path';
|
|
32
30
|
import { assertDocsAiDeployment, writeDocsAiFileAtomic } from './atomic-write.mjs';
|
|
31
|
+
import { isDirectRun, sameFile } from './direct-run.mjs';
|
|
33
32
|
import {
|
|
34
33
|
AUTHORED_KEYS,
|
|
35
34
|
SOURCE_SIZE_CONFIG_REL,
|
|
@@ -299,20 +298,11 @@ export const main = (argv, ctx = {}) => {
|
|
|
299
298
|
}
|
|
300
299
|
};
|
|
301
300
|
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
export const sameFile = (a, b) => {
|
|
308
|
-
try {
|
|
309
|
-
return realpathSync(a) === realpathSync(b);
|
|
310
|
-
} catch {
|
|
311
|
-
return false;
|
|
312
|
-
}
|
|
313
|
-
};
|
|
314
|
-
const isDirectRun = Boolean(process.argv[1]) && sameFile(fileURLToPath(import.meta.url), process.argv[1]);
|
|
315
|
-
if (isDirectRun) {
|
|
301
|
+
// The realpath-compare direct-run predicate now lives in tools/direct-run.mjs (the fix this file
|
|
302
|
+
// carried first, extracted so every module shares one implementation); `sameFile` stays exported here
|
|
303
|
+
// because this module's tests bind it as a seam.
|
|
304
|
+
export { sameFile };
|
|
305
|
+
if (isDirectRun(import.meta.url)) {
|
|
316
306
|
const result = main(process.argv.slice(2));
|
|
317
307
|
if (result.stdout) process.stdout.write(result.stdout.endsWith('\n') ? result.stdout : `${result.stdout}\n`);
|
|
318
308
|
if (result.stderr) process.stderr.write(result.stderr.endsWith('\n') ? result.stderr : `${result.stderr}\n`);
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
// • source-size-refusal.mjs — the two exit classes, and the absolute config path every refusal names
|
|
8
8
|
// • source-size-config.mjs — the config file: its grammar, its four states, its reader
|
|
9
9
|
// • source-size-scope.mjs — which files are judged (D-6) and how big each one is (D-7)
|
|
10
|
-
// • source-size-gate-cmd.mjs — whether a declared gate cmd IS this checker (the canonical matcher)
|
|
10
|
+
// • source-size-gate-cmd.mjs — whether a declared gate cmd IS this checker (the canonical matcher),
|
|
11
|
+
// and which of the three tool claims it makes when it is not
|
|
11
12
|
//
|
|
12
13
|
// Re-export only: a consumer imports the practice, never a particular half, so a later split moves
|
|
13
14
|
// code without touching a single call site.
|
|
@@ -48,6 +49,11 @@ export {
|
|
|
48
49
|
export {
|
|
49
50
|
SOURCE_SIZE_GATE_ID,
|
|
50
51
|
SOURCE_SIZE_TOOL_PATH,
|
|
52
|
+
classifySourceSizeGate,
|
|
51
53
|
dqUnsafePath,
|
|
52
54
|
matchesSourceSizeGate,
|
|
53
55
|
} from './source-size-gate-cmd.mjs';
|
|
56
|
+
|
|
57
|
+
// The claim vocabulary itself — a consumer naming an outcome imports the name, never a string
|
|
58
|
+
// literal it could misspell into a silently-never-true comparison.
|
|
59
|
+
export { CHECKER_CLAIM } from './checker-claim.mjs';
|