@sabaiway/agent-workflow-kit 14.10.0 → 14.12.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 +82 -0
- package/README.md +1 -1
- package/SKILL.md +4 -4
- package/capability.json +1 -1
- package/migrations/4.0.0-full-flow-offer.md +33 -0
- package/migrations/README.md +2 -2
- package/package.json +1 -1
- package/references/modes/bootstrap.md +1 -1
- package/references/modes/control-bytes.md +9 -6
- package/references/modes/doc-parity.md +1 -1
- package/references/modes/gates.md +3 -1
- package/references/modes/hook.md +1 -1
- package/references/modes/upgrade.md +21 -7
- package/references/modes/velocity.md +1 -1
- package/references/scripts/migrate-gates.mjs +11 -9
- package/tools/cheap-agents-read.mjs +1 -1
- package/tools/checker-claim.mjs +12 -10
- package/tools/checker-gates-read.mjs +156 -0
- package/tools/checker-gates.mjs +134 -0
- package/tools/direct-run.mjs +4 -0
- package/tools/doc-parity.mjs +2 -2
- package/tools/gates-declaration.mjs +15 -1
- package/tools/gates-init.mjs +3 -3
- package/tools/profile-gaps.mjs +86 -0
- package/tools/reference-profile.mjs +69 -0
- package/tools/tier-preview.mjs +248 -0
- package/tools/upgrade-runlist.mjs +6 -0
- package/tools/velocity-profile.mjs +1 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// checker-gates-read.mjs — the READ-ONLY leaf of the kit-owned checker gates: the frozen candidate
|
|
2
|
+
// table, the precondition probes, the declaration read and the one derivation the declare verb and
|
|
3
|
+
// the profile-gap detector both call. Governing contract: docs/ai/specs/kit/tier/checker-gates/
|
|
4
|
+
// (part checker-gates-read). No CLI (refuseDirectRun); nothing here writes.
|
|
5
|
+
import { lstatSync } from 'node:fs';
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
|
+
import { CHECKER_CLAIM, classifyCheckerClaim, dqUnsafePath } from './checker-claim.mjs';
|
|
8
|
+
import { refuseDirectRun } from './direct-run.mjs';
|
|
9
|
+
import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
|
|
10
|
+
import { GATES_REL, KIT_CHECKER_CLAIMS, coverageDeclarationDefects, validateDeclaration } from './gates-declaration.mjs';
|
|
11
|
+
import { escapeForLine, isLineUnsafe } from './source-size-core.mjs';
|
|
12
|
+
|
|
13
|
+
const DEPLOYMENT_CHAIN = ['.', 'docs', 'docs/ai'];
|
|
14
|
+
const NOT_APPLICABLE_CODES = new Set(['ENOENT', 'ENOTDIR']);
|
|
15
|
+
const OK = 'ok';
|
|
16
|
+
const ABSENT = 'absent';
|
|
17
|
+
const FOREIGN = 'foreign';
|
|
18
|
+
const SYMLINK_CLASS = 'symlink';
|
|
19
|
+
const GIT_DIR = Object.freeze({ rel: '.git', kinds: ['directory', 'file'], what: 'a directory or a regular gitfile' });
|
|
20
|
+
const STORE_ROOT = Object.freeze({ rel: 'docs/ai/specs/index.md', kinds: ['file'], what: 'a regular file' });
|
|
21
|
+
const COVERAGE_SCOPE = Object.freeze({ rel: 'docs/ai/spec-coverage.json', kinds: ['file'], what: 'a regular file' });
|
|
22
|
+
|
|
23
|
+
export const CANDIDATE_STATES = Object.freeze({
|
|
24
|
+
declared: 'declared',
|
|
25
|
+
idTaken: 'id-taken',
|
|
26
|
+
probeUnreadable: 'probe-unreadable',
|
|
27
|
+
notApplicable: 'not-applicable',
|
|
28
|
+
offered: 'offered',
|
|
29
|
+
withheld: 'withheld',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const lstatOf = (deps) => deps.lstatSync ?? lstatSync;
|
|
33
|
+
|
|
34
|
+
export const isDeployed = (root, deps) => {
|
|
35
|
+
try {
|
|
36
|
+
return DEPLOYMENT_CHAIN.every((path, index) => {
|
|
37
|
+
const stat = lstatOf(deps)(resolve(root, path));
|
|
38
|
+
if (stat.isSymbolicLink()) return false;
|
|
39
|
+
return index < DEPLOYMENT_CHAIN.length - 1 || stat.isDirectory();
|
|
40
|
+
});
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const kindAt = (path, lstat) => {
|
|
47
|
+
try {
|
|
48
|
+
const stat = lstat(path);
|
|
49
|
+
if (stat.isSymbolicLink()) return { kind: SYMLINK_CLASS };
|
|
50
|
+
if (stat.isDirectory()) return { kind: 'directory' };
|
|
51
|
+
return { kind: stat.isFile() ? 'file' : 'other' };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (NOT_APPLICABLE_CODES.has(error?.code)) return { kind: ABSENT };
|
|
54
|
+
return { unreadable: String(error?.code ?? error?.message ?? error) };
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// A probe answers {} when its checker runs by design over the tree, else the state and its detail.
|
|
59
|
+
const requiring = (...requirements) => (root, deps) => {
|
|
60
|
+
const judged = requirements.map((requirement) => ({ requirement, ...kindAt(resolve(root, requirement.rel), lstatOf(deps)) }));
|
|
61
|
+
const unreadable = judged.find((item) => item.unreadable !== undefined);
|
|
62
|
+
if (unreadable) {
|
|
63
|
+
return { state: CANDIDATE_STATES.probeUnreadable, detail: `${unreadable.requirement.rel}: ${unreadable.unreadable}` };
|
|
64
|
+
}
|
|
65
|
+
const missing = judged.find(({ requirement, kind }) => !requirement.kinds.includes(kind));
|
|
66
|
+
if (missing) return { state: CANDIDATE_STATES.notApplicable, detail: `${missing.requirement.rel} is not ${missing.requirement.what}` };
|
|
67
|
+
return {};
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const candidate = (id, title, probe) => Object.freeze({ id, title, probe });
|
|
71
|
+
|
|
72
|
+
export const CHECKER_GATE_CANDIDATES = Object.freeze([
|
|
73
|
+
candidate('control-bytes', 'No raw control byte in the work tree', requiring(GIT_DIR)),
|
|
74
|
+
candidate('plan-shape', 'Plans in flight keep the structural planning shape', requiring()),
|
|
75
|
+
candidate('spec-check', 'Feature-spec store structure — the whole store', requiring(STORE_ROOT)),
|
|
76
|
+
candidate(
|
|
77
|
+
'spec-coverage',
|
|
78
|
+
'No work without a specification — every in-scope tool is governed by a contract, or named in the shrink-only debt',
|
|
79
|
+
requiring(COVERAGE_SCOPE, STORE_ROOT),
|
|
80
|
+
),
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const claimOf = (id) => KIT_CHECKER_CLAIMS.find((claim) => claim.id === id);
|
|
84
|
+
|
|
85
|
+
const refusal = (name, detail) => (detail === undefined ? { refusal: name } : { refusal: name, detail: String(detail) });
|
|
86
|
+
|
|
87
|
+
const attempt = (read) => {
|
|
88
|
+
try {
|
|
89
|
+
return { value: read() };
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return { error };
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const readDeclaration = (root, deps) => {
|
|
96
|
+
const read = readRegularFileNoFollow(resolve(root, GATES_REL), deps);
|
|
97
|
+
if (read.outcome === ABSENT) return refusal('declaration-absent');
|
|
98
|
+
if (read.outcome === FOREIGN) {
|
|
99
|
+
return read.className === SYMLINK_CLASS ? refusal('declaration-symlink') : refusal('declaration-not-regular', read.className);
|
|
100
|
+
}
|
|
101
|
+
if (read.outcome !== OK) return refusal('declaration-unreadable', read.code);
|
|
102
|
+
const parsed = attempt(() => JSON.parse(read.content));
|
|
103
|
+
if (parsed.error) return refusal('declaration-malformed', parsed.error.message);
|
|
104
|
+
const validated = attempt(() => validateDeclaration(parsed.value));
|
|
105
|
+
if (validated.error) return refusal('declaration-invalid', validated.error.message);
|
|
106
|
+
const gates = validated.value;
|
|
107
|
+
const defects = coverageDeclarationDefects(gates, root);
|
|
108
|
+
if (defects.length > 0) return refusal('declaration-coverage-defect', defects[0].message);
|
|
109
|
+
return { declaration: Object.hasOwn(parsed.value, '_README') ? { readme: parsed.value._README, gates } : { gates } };
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const claimState = ({ id, screen }, gates, root) => {
|
|
113
|
+
const claimed = gates.some((gate) => {
|
|
114
|
+
const claim = classifyCheckerClaim(screen, gate.cmd, root);
|
|
115
|
+
return claim === CHECKER_CLAIM.CANONICAL || claim === CHECKER_CLAIM.ELSEWHERE;
|
|
116
|
+
});
|
|
117
|
+
if (claimed) return { state: CANDIDATE_STATES.declared };
|
|
118
|
+
const taken = gates.find((gate) => gate.id === id);
|
|
119
|
+
return taken ? { state: CANDIDATE_STATES.idTaken, detail: taken.cmd } : {};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const toolPathOf = ({ basename, screen }, deps) => join(deps.kitToolsDir ?? dirname(screen.canonical), basename);
|
|
123
|
+
const isUnrenderable = (path) => dqUnsafePath(path) || isLineUnsafe(path);
|
|
124
|
+
|
|
125
|
+
const judgeCandidate = ({ id, title, probe }, rendered, gates, root, deps) => {
|
|
126
|
+
const claimed = claimState(claimOf(id), gates, root);
|
|
127
|
+
if (claimed.state) return claimed;
|
|
128
|
+
const probed = probe(root, deps);
|
|
129
|
+
if (probed.state) return probed;
|
|
130
|
+
if (rendered.unrenderable) return { state: CANDIDATE_STATES.withheld };
|
|
131
|
+
return { state: CANDIDATE_STATES.offered, entry: JSON.stringify({ id, title, cmd: rendered.cmd }) };
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const shapeOf = (id, { state, entry = null, detail }) => (detail === undefined ? { id, state, entry } : { id, state, entry, detail });
|
|
135
|
+
|
|
136
|
+
export const judgeCheckerGates = (root, deps = {}) => {
|
|
137
|
+
if (!isDeployed(root, deps)) return refusal('no-deployment');
|
|
138
|
+
const loaded = readDeclaration(root, deps);
|
|
139
|
+
if (loaded.refusal) return loaded;
|
|
140
|
+
const rendered = CHECKER_GATE_CANDIDATES.map(({ id }) => {
|
|
141
|
+
const claim = claimOf(id);
|
|
142
|
+
const path = toolPathOf(claim, deps);
|
|
143
|
+
return isUnrenderable(path) ? { unrenderable: path } : { cmd: `node "${path}" ${claim.tail}` };
|
|
144
|
+
});
|
|
145
|
+
const judged = CHECKER_GATE_CANDIDATES.map((entry, index) => judgeCandidate(entry, rendered[index], loaded.declaration.gates, root, deps));
|
|
146
|
+
const firstWithheld = judged.findIndex(({ state }) => state === CANDIDATE_STATES.withheld);
|
|
147
|
+
const named = (item, index) => (index === firstWithheld
|
|
148
|
+
? { ...item, detail: `the kit path cannot be rendered into a command: ${escapeForLine(rendered[index].unrenderable)}` }
|
|
149
|
+
: item);
|
|
150
|
+
return {
|
|
151
|
+
declaration: loaded.declaration,
|
|
152
|
+
candidates: CHECKER_GATE_CANDIDATES.map(({ id }, index) => shapeOf(id, named(judged[index], index))),
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
refuseDirectRun(import.meta.url);
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// checker-gates.mjs — the declare verb of the kit-owned checker gates: previews the candidates the
|
|
3
|
+
// read leaf judges and, on --apply, writes the offered ones add-only into docs/ai/gates.json.
|
|
4
|
+
// Governing contract: docs/ai/specs/kit/tier/checker-gates/ (part checker-gates-verb).
|
|
5
|
+
// main(argv, deps) returns, never exits, never asks.
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import { assertDocsAiDeployment, writeDocsAiFileAtomic } from './atomic-write.mjs';
|
|
8
|
+
import { safeLine } from './carriers.mjs';
|
|
9
|
+
import { CANDIDATE_STATES, judgeCheckerGates } from './checker-gates-read.mjs';
|
|
10
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
11
|
+
import { GATES_REL, coverageDeclarationDefects, validateDeclaration } from './gates-declaration.mjs';
|
|
12
|
+
import { TRUST_CHAIN_DISCLOSURE, placeEntries } from './gates-init.mjs';
|
|
13
|
+
|
|
14
|
+
const LF = String.fromCharCode(10);
|
|
15
|
+
const ARGV_OFFSET = 2;
|
|
16
|
+
const EXIT_SUCCESS = 0;
|
|
17
|
+
const EXIT_REFUSAL = 1;
|
|
18
|
+
const EXIT_USAGE = 2;
|
|
19
|
+
const CWD_FLAG = '--cwd';
|
|
20
|
+
const APPLY_FLAG = '--apply';
|
|
21
|
+
const ONLY_FLAG = '--only';
|
|
22
|
+
const HELP_FLAGS = new Set(['--help', '-h']);
|
|
23
|
+
const APPLIED = 'applied';
|
|
24
|
+
const INDENT = ' ';
|
|
25
|
+
const JSON_INDENT = 2;
|
|
26
|
+
const NOUN = 'a gate declaration';
|
|
27
|
+
|
|
28
|
+
export const OUTCOME_LINES = Object.freeze({
|
|
29
|
+
candidate: (id, word, detail) => `${id}: ${word}${detail ? ` — ${safeLine(detail)}` : ''}`,
|
|
30
|
+
entry: (json) => `${INDENT}entry: ${json}`,
|
|
31
|
+
disclosure: () => TRUST_CHAIN_DISCLOSURE,
|
|
32
|
+
refusal: (name, detail) => `checker-gates: refused — ${name}${detail ? `: ${safeLine(detail)}` : ''}`,
|
|
33
|
+
usage: (problem) => `usage: checker-gates.mjs --cwd DIR [--apply] [--only ID]... — ${safeLine(problem)}`,
|
|
34
|
+
onlyNotOffered: (ids, offered) => `${ONLY_FLAG} names ids not offered: ${ids.join(', ')} (offered: ${offered.join(', ') || 'none'})`,
|
|
35
|
+
help: () => [
|
|
36
|
+
'checker-gates.mjs --cwd DIR [--apply] [--only ID]...',
|
|
37
|
+
'Previews the kit-owned checker gates the project in DIR can declare; writes nothing by default.',
|
|
38
|
+
'--apply writes the offered entries into docs/ai/gates.json, add-only; --only ID narrows the write.',
|
|
39
|
+
'Exit codes: 0 done, 1 one named refusal, 2 usage.',
|
|
40
|
+
].join(LF),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const result = (code, stdout = '', stderr = '') => ({ code, stdout, stderr });
|
|
44
|
+
const refuse = (name, detail) => result(EXIT_REFUSAL, '', OUTCOME_LINES.refusal(name, detail));
|
|
45
|
+
const causeOf = (error) => String(error?.message ?? error);
|
|
46
|
+
|
|
47
|
+
const parseArgs = (argv) => {
|
|
48
|
+
if (argv.length === 1 && HELP_FLAGS.has(argv[0])) return { help: true };
|
|
49
|
+
const options = { only: [] };
|
|
50
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
51
|
+
const flag = argv[index];
|
|
52
|
+
const value = argv[index + 1];
|
|
53
|
+
if (flag === CWD_FLAG) {
|
|
54
|
+
if (options.cwd !== undefined) return { problem: `${CWD_FLAG} given twice` };
|
|
55
|
+
if (value === undefined || value.startsWith('-')) return { problem: `${CWD_FLAG} needs a directory` };
|
|
56
|
+
options.cwd = value;
|
|
57
|
+
index += 1;
|
|
58
|
+
} else if (flag === APPLY_FLAG) {
|
|
59
|
+
if (options.apply) return { problem: `${APPLY_FLAG} given twice` };
|
|
60
|
+
options.apply = true;
|
|
61
|
+
} else if (flag === ONLY_FLAG) {
|
|
62
|
+
if (value === undefined || value.startsWith('-')) return { problem: `${ONLY_FLAG} needs a gate id` };
|
|
63
|
+
if (!options.only.includes(value)) options.only.push(value);
|
|
64
|
+
index += 1;
|
|
65
|
+
} else {
|
|
66
|
+
return { problem: `unknown argument ${flag}` };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (options.cwd === undefined) return { problem: `${CWD_FLAG} is required` };
|
|
70
|
+
return { cwd: options.cwd, apply: Boolean(options.apply), only: options.only };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const mergeCheckerGates = (loaded, selected, root) => {
|
|
74
|
+
const readme = Object.hasOwn(loaded, 'readme') ? { _README: loaded.readme } : {};
|
|
75
|
+
const merged = { ...readme, gates: placeEntries(loaded.gates, selected, root) };
|
|
76
|
+
validateDeclaration(merged);
|
|
77
|
+
const defects = coverageDeclarationDefects(merged.gates, root);
|
|
78
|
+
if (defects.length > 0) return { refusal: 'coverage-defect', detail: defects[0].message };
|
|
79
|
+
return { merged, body: `${JSON.stringify(merged, null, JSON_INDENT)}${LF}` };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const renderLines = (candidates) => {
|
|
83
|
+
const disclose = candidates.some(({ state }) => state === CANDIDATE_STATES.offered || state === APPLIED);
|
|
84
|
+
const lines = candidates.flatMap(({ id, state, entry, detail }) => (state === CANDIDATE_STATES.offered
|
|
85
|
+
? [OUTCOME_LINES.candidate(id, state, detail), OUTCOME_LINES.entry(entry)]
|
|
86
|
+
: [OUTCOME_LINES.candidate(id, state, detail)]));
|
|
87
|
+
return disclose ? [OUTCOME_LINES.disclosure(), ...lines] : lines;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const writeSelected = (root, judged, selected, deps) => {
|
|
91
|
+
const merge = mergeCheckerGates(judged.declaration, selected.map(({ entry }) => JSON.parse(entry)), root);
|
|
92
|
+
if (merge.refusal) return { refusal: refuse(merge.refusal, merge.detail) };
|
|
93
|
+
try {
|
|
94
|
+
writeDocsAiFileAtomic(root, GATES_REL, merge.body, deps, { noun: NOUN, rel: GATES_REL });
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return { refusal: refuse('write-failed', causeOf(error)) };
|
|
97
|
+
}
|
|
98
|
+
const written = new Set(selected.map(({ id }) => id));
|
|
99
|
+
return { candidates: judged.candidates.map((item) => (written.has(item.id) ? { ...item, state: APPLIED, entry: null } : item)) };
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const runVerb = (options, deps) => {
|
|
103
|
+
const root = resolve(options.cwd);
|
|
104
|
+
try {
|
|
105
|
+
assertDocsAiDeployment(root, deps, { noun: NOUN, rel: GATES_REL });
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return refuse('no-deployment', causeOf(error));
|
|
108
|
+
}
|
|
109
|
+
const judged = judgeCheckerGates(root, deps);
|
|
110
|
+
if (judged.refusal) return refuse(judged.refusal, judged.detail);
|
|
111
|
+
const offered = judged.candidates.filter(({ state }) => state === CANDIDATE_STATES.offered);
|
|
112
|
+
const offeredIds = offered.map(({ id }) => id);
|
|
113
|
+
const unknown = options.only.filter((id) => !offeredIds.includes(id));
|
|
114
|
+
if (unknown.length > 0) return result(EXIT_USAGE, '', OUTCOME_LINES.usage(OUTCOME_LINES.onlyNotOffered(unknown, offeredIds)));
|
|
115
|
+
const selected = options.only.length > 0 ? offered.filter(({ id }) => options.only.includes(id)) : offered;
|
|
116
|
+
if (!options.apply || selected.length === 0) return result(EXIT_SUCCESS, renderLines(judged.candidates).join(LF));
|
|
117
|
+
const written = writeSelected(root, judged, selected, deps);
|
|
118
|
+
if (written.refusal) return written.refusal;
|
|
119
|
+
return result(EXIT_SUCCESS, renderLines(written.candidates).join(LF));
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const main = (argv, deps = {}) => {
|
|
123
|
+
const parsed = parseArgs(argv);
|
|
124
|
+
if (parsed.help) return result(EXIT_SUCCESS, OUTCOME_LINES.help());
|
|
125
|
+
if (parsed.problem) return result(EXIT_USAGE, '', OUTCOME_LINES.usage(parsed.problem));
|
|
126
|
+
return runVerb(parsed, deps);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (isDirectRun(import.meta.url)) {
|
|
130
|
+
const outcome = main(process.argv.slice(ARGV_OFFSET));
|
|
131
|
+
if (outcome.stdout) console.log(outcome.stdout);
|
|
132
|
+
if (outcome.stderr) console.error(outcome.stderr);
|
|
133
|
+
process.exitCode = outcome.code;
|
|
134
|
+
}
|
package/tools/direct-run.mjs
CHANGED
|
@@ -65,6 +65,10 @@ export const LIBRARY_ONLY_MODULES = Object.freeze({
|
|
|
65
65
|
'review-roster.mjs': '/agent-workflow-kit recipes',
|
|
66
66
|
'review-roster-resolve.mjs': '/agent-workflow-kit recipes',
|
|
67
67
|
'set-recipe-roster.mjs': '/agent-workflow-kit set-recipe',
|
|
68
|
+
// The read half of the tier offer: the upgrade's step-3 `tier` item runs the offer that uses it.
|
|
69
|
+
'reference-profile.mjs': '/agent-workflow-kit upgrade',
|
|
70
|
+
// The read half of the checker-gates verb: the same step-3 `tier` item runs the verb that uses it.
|
|
71
|
+
'checker-gates-read.mjs': '/agent-workflow-kit upgrade',
|
|
68
72
|
});
|
|
69
73
|
|
|
70
74
|
// The frozen refusal line. One line, names the module, names the command.
|
package/tools/doc-parity.mjs
CHANGED
|
@@ -330,8 +330,8 @@ read only from the surface anchored between the doc's advisor-matrix begin/end m
|
|
|
330
330
|
elsewhere in the doc can neither stand in for it nor mask its drift, and a drifted header inside the
|
|
331
331
|
anchor fails closed rather than falling through to another table.
|
|
332
332
|
|
|
333
|
-
--check exits 0/1 as a gate
|
|
334
|
-
result. Default prints the per-binding report.
|
|
333
|
+
--check exits 0/1 as a gate over the kit's own package, the gate a kit maintainer runs. --json prints
|
|
334
|
+
the structured result. Default prints the per-binding report.
|
|
335
335
|
|
|
336
336
|
Read-only: never writes, never commits, spawns nothing. Exit codes: 0 pass (or plain report); 1 drift
|
|
337
337
|
(under --check) or error; 2 usage.`;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { readFileSync, lstatSync, realpathSync } from 'node:fs';
|
|
10
10
|
import { join, isAbsolute } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { CHECKER_CLAIM, checkerClaimTool, classifyCheckerClaim } from './checker-claim.mjs';
|
|
12
13
|
import { fail, loadConfig, CONFIG_REL } from './orchestration-config.mjs';
|
|
13
14
|
import { isCoverageProducerGate } from './coverage-producer.mjs';
|
|
14
15
|
import { matchesSourceSizeGate } from './source-size-core.mjs';
|
|
@@ -224,8 +225,21 @@ export const isReviewDependentGate = (gate, projectDir) =>
|
|
|
224
225
|
// "does this declaration verify the PROJECT?" through the review-dependent predicate alone read a
|
|
225
226
|
// matrix of nothing but that gate as project verification. Three surfaces asked it, each knowing a
|
|
226
227
|
// different half; this is the one home, so a future kit checker is added once.
|
|
228
|
+
// KIT_CHECKER_CLAIMS is the one statement of the four declarable kit checkers' basenames, tails and
|
|
229
|
+
// canonical paths (docs/ai/specs/kit/tier/checker-gates/, part checker-claim-tail).
|
|
230
|
+
const kitCheckerClaim = (id, basename, tail) => Object.freeze({
|
|
231
|
+
id, basename, tail, screen: checkerClaimTool(basename, fileURLToPath(new URL(`./${basename}`, import.meta.url)), tail),
|
|
232
|
+
});
|
|
233
|
+
export const KIT_CHECKER_CLAIMS = Object.freeze([
|
|
234
|
+
kitCheckerClaim('control-bytes', 'control-bytes.mjs', '--check'),
|
|
235
|
+
kitCheckerClaim('plan-shape', 'plan-shape-cli.mjs', '--check --in-flight'),
|
|
236
|
+
kitCheckerClaim('spec-check', 'spec-check-cli.mjs', '--all'),
|
|
237
|
+
kitCheckerClaim('spec-coverage', 'spec-coverage-cli.mjs', '--check'),
|
|
238
|
+
]);
|
|
239
|
+
|
|
227
240
|
export const isKitOwnedCheckerGate = (gate, projectDir) =>
|
|
228
|
-
isReviewDependentGate(gate, projectDir) || matchesSourceSizeGate(gate.cmd, projectDir)
|
|
241
|
+
isReviewDependentGate(gate, projectDir) || matchesSourceSizeGate(gate.cmd, projectDir)
|
|
242
|
+
|| KIT_CHECKER_CLAIMS.some(({ screen }) => classifyCheckerClaim(screen, gate.cmd, projectDir) === CHECKER_CLAIM.CANONICAL);
|
|
229
243
|
|
|
230
244
|
// ── the pregate subset derivation (#66 / Decision 7 — ONE home for producer and factory) ─────
|
|
231
245
|
|
package/tools/gates-init.mjs
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
// explicitly-consenting prose (the init/bootstrap flows and the gates.md consent-fill section) —
|
|
5
5
|
// it is NOT a routable mode token, it sits OUTSIDE every velocity allowlist tier (a
|
|
6
6
|
// consent-per-run writer is never pre-approved), and the shipped gates.json TEMPLATE stays EMPTY
|
|
7
|
-
// (AD-021/AD-038): a populated declaration is per-entry maintainer consent recorded through
|
|
8
|
-
// preview, never auto-seeding.
|
|
9
|
-
//
|
|
7
|
+
// (AD-021/AD-038): a populated declaration is per-entry maintainer consent recorded through a
|
|
8
|
+
// consented writer (this preview among them), never auto-seeding. This preview runs at INIT; at UPGRADE step 3's
|
|
9
|
+
// run-list changes entries only through the consented legacy migration and, under `tier`, checker-gates.mjs.
|
|
10
10
|
//
|
|
11
11
|
// What it offers (the derivation invariants, test-pinned) — CLOSED-WORLD since AD-052: an entry
|
|
12
12
|
// is offered only when every axis is proven safe by MEMBERSHIP in a finite, test-guarded set,
|
package/tools/profile-gaps.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { CANDIDATE_STATES, isDeployed, judgeCheckerGates } from './checker-gates-read.mjs';
|
|
4
|
+
import { CONFIG_REL, validateConfig } from './orchestration-config.mjs';
|
|
5
|
+
import { TIER_TARGETS } from './reference-profile.mjs';
|
|
3
6
|
import {
|
|
4
7
|
RULES_REGIONS,
|
|
5
8
|
extractRegionBy,
|
|
@@ -11,6 +14,13 @@ import {
|
|
|
11
14
|
} from './rules-regions.mjs';
|
|
12
15
|
|
|
13
16
|
const GAP_ID = 'story-sessions-section';
|
|
17
|
+
const SLOTS_ID = 'epic-task-slots';
|
|
18
|
+
const STORE_ID = 'epic-store-seeded';
|
|
19
|
+
const STORE_PATH = 'docs/ai/epics';
|
|
20
|
+
const CHECKER_GATES_ID = 'checker-gates-declared';
|
|
21
|
+
const TIER_PREVIEW_PATH = fileURLToPath(new URL('./tier-preview.mjs', import.meta.url));
|
|
22
|
+
const CHECKER_GATES_PATH = fileURLToPath(new URL('./checker-gates.mjs', import.meta.url));
|
|
23
|
+
const UNDECIDED_STATES = [CANDIDATE_STATES.idTaken, CANDIDATE_STATES.probeUnreadable, CANDIDATE_STATES.withheld];
|
|
14
24
|
const STORY_ID = 'story-sessions';
|
|
15
25
|
const TEMPLATE_CANON = 'template';
|
|
16
26
|
const ENCODING = 'utf8';
|
|
@@ -28,6 +38,15 @@ const REASONS = Object.freeze({
|
|
|
28
38
|
fileAbsent: 'file-absent',
|
|
29
39
|
fileSymlink: 'file-symlink',
|
|
30
40
|
fileUnreadable: 'file-unreadable',
|
|
41
|
+
noDeployment: 'no-deployment',
|
|
42
|
+
configAbsent: 'config-absent',
|
|
43
|
+
configSymlink: 'config-symlink',
|
|
44
|
+
configUnreadable: 'config-unreadable',
|
|
45
|
+
configMalformed: 'config-malformed',
|
|
46
|
+
configInvalid: 'config-invalid',
|
|
47
|
+
storeNotDirectory: 'store-not-directory',
|
|
48
|
+
storeSymlink: 'store-symlink',
|
|
49
|
+
storeUnreadable: 'store-unreadable',
|
|
31
50
|
});
|
|
32
51
|
const TEMPLATE_REGIONS = RULES_REGIONS.filter(({ canon }) => canon === TEMPLATE_CANON);
|
|
33
52
|
const STORY_REGION = RULES_REGIONS.find(({ id }) => id === STORY_ID);
|
|
@@ -81,6 +100,73 @@ const detectStorySessions = ({ root, deps }) => {
|
|
|
81
100
|
return { verdict: VERDICTS.absent };
|
|
82
101
|
};
|
|
83
102
|
|
|
103
|
+
const parseConfig = (bytes) => {
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = JSON.parse(decodeUtf8Strict(bytes));
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return error instanceof SyntaxError ? { reason: REASONS.configMalformed } : { reason: REASONS.configUnreadable };
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
return { config: validateConfig(parsed) };
|
|
112
|
+
} catch {
|
|
113
|
+
return { reason: REASONS.configInvalid };
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const readTierConfig = (root, deps) => {
|
|
118
|
+
const path = resolve(root, CONFIG_REL);
|
|
119
|
+
try {
|
|
120
|
+
const stat = deps.lstatSync(path);
|
|
121
|
+
if (stat.isSymbolicLink()) return { reason: REASONS.configSymlink };
|
|
122
|
+
if (!stat.isFile()) return { reason: REASONS.configUnreadable };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return { reason: error?.code === MISSING_FILE_CODE ? REASONS.configAbsent : REASONS.configUnreadable };
|
|
125
|
+
}
|
|
126
|
+
let bytes;
|
|
127
|
+
try {
|
|
128
|
+
bytes = deps.readFileSync(path);
|
|
129
|
+
} catch {
|
|
130
|
+
return { reason: REASONS.configUnreadable };
|
|
131
|
+
}
|
|
132
|
+
return parseConfig(bytes);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const detectSlots = ({ root, deps }) => {
|
|
136
|
+
if (!isDeployed(root, deps)) return reportUndecidable(REASONS.noDeployment);
|
|
137
|
+
const read = readTierConfig(root, deps);
|
|
138
|
+
if (read.reason) return reportUndecidable(read.reason);
|
|
139
|
+
const declared = TIER_TARGETS.every(({ activity, slot }) => Object.hasOwn(read.config[activity] ?? {}, slot));
|
|
140
|
+
return { verdict: declared ? VERDICTS.present : VERDICTS.absent };
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const detectStore = ({ root, deps }) => {
|
|
144
|
+
if (!isDeployed(root, deps)) return reportUndecidable(REASONS.noDeployment);
|
|
145
|
+
let stat;
|
|
146
|
+
try {
|
|
147
|
+
stat = deps.lstatSync(resolve(root, STORE_PATH));
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error?.code === MISSING_FILE_CODE) return { verdict: VERDICTS.absent };
|
|
150
|
+
return reportUndecidable(REASONS.storeUnreadable);
|
|
151
|
+
}
|
|
152
|
+
if (stat.isSymbolicLink()) return reportUndecidable(REASONS.storeSymlink);
|
|
153
|
+
if (!stat.isDirectory()) return reportUndecidable(REASONS.storeNotDirectory);
|
|
154
|
+
return { verdict: VERDICTS.present };
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const detectCheckerGates = ({ root, deps }) => {
|
|
158
|
+
const judged = judgeCheckerGates(root, deps);
|
|
159
|
+
if (judged.refusal) return reportUndecidable(judged.refusal);
|
|
160
|
+
if (judged.candidates.some(({ state }) => state === CANDIDATE_STATES.offered)) return { verdict: VERDICTS.absent };
|
|
161
|
+
const undecided = judged.candidates.find(({ state }) => UNDECIDED_STATES.includes(state));
|
|
162
|
+
return undecided ? reportUndecidable(undecided.state) : { verdict: VERDICTS.present };
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const tierPreview = (root) => buildInsertPreview(root, TIER_PREVIEW_PATH);
|
|
166
|
+
|
|
84
167
|
export const PROFILE_GAPS = Object.freeze([
|
|
85
168
|
Object.freeze({ id: GAP_ID, detect: detectStorySessions, apply: (root) => buildInsertPreview(root) }),
|
|
169
|
+
Object.freeze({ id: SLOTS_ID, detect: detectSlots, apply: tierPreview }),
|
|
170
|
+
Object.freeze({ id: STORE_ID, detect: detectStore, apply: tierPreview }),
|
|
171
|
+
Object.freeze({ id: CHECKER_GATES_ID, detect: detectCheckerGates, apply: (root) => buildInsertPreview(root, CHECKER_GATES_PATH) }),
|
|
86
172
|
]);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// reference-profile.mjs — the READ-ONLY leaf over the running kit's reference profile and a
|
|
2
|
+
// project's decline record: the frozen epic/task target table, the shipped profile reader, the
|
|
3
|
+
// decline record reader and the currency rule. Governing contract: docs/ai/specs/kit/tier/tier-offer/.
|
|
4
|
+
// No CLI (refuseDirectRun); every read goes through injected deps; nothing here writes.
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
|
|
8
|
+
import { refuseDirectRun } from './direct-run.mjs';
|
|
9
|
+
|
|
10
|
+
const PROFILE_PATH = fileURLToPath(new URL('../references/reference-profile.json', import.meta.url));
|
|
11
|
+
const DECLINES_REL = 'docs/ai/profile-declines.json';
|
|
12
|
+
const PROFILE_KEYS = ['items', 'lineage', 'name', 'schema'];
|
|
13
|
+
const ITEM_KEYS = ['has', 'id', 'story'];
|
|
14
|
+
const RECORD_KEYS = ['declined', 'schema'];
|
|
15
|
+
const SCHEMA = 1;
|
|
16
|
+
const OK = 'ok';
|
|
17
|
+
const ABSENT = 'absent';
|
|
18
|
+
const SYMLINK_CLASS = 'symlink';
|
|
19
|
+
|
|
20
|
+
const target = (activity, slot, candidates) => Object.freeze({ activity, slot, candidates: Object.freeze(candidates) });
|
|
21
|
+
|
|
22
|
+
export const TIER_TARGETS = Object.freeze([
|
|
23
|
+
target('epic', 'author', ['subagent', 'solo']),
|
|
24
|
+
target('epic', 'review', ['reviewed', 'solo']),
|
|
25
|
+
target('task', 'author', ['delegated', 'subagent', 'solo']),
|
|
26
|
+
target('task', 'execute', ['delegated', 'subagent', 'solo']),
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
30
|
+
const hasKeys = (value, keys) => isPlainObject(value) && Object.keys(value).sort().join() === keys.join();
|
|
31
|
+
const isText = (value) => typeof value === 'string' && value.length > 0;
|
|
32
|
+
const parseJson = (text) => {
|
|
33
|
+
try {
|
|
34
|
+
return { value: JSON.parse(text) };
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const isProfile = (value) => hasKeys(value, PROFILE_KEYS) && value.schema === SCHEMA
|
|
41
|
+
&& isText(value.name) && isText(value.lineage) && Array.isArray(value.items)
|
|
42
|
+
&& value.items.every((item) => hasKeys(item, ITEM_KEYS) && isText(item.id) && isText(item.story) && isText(item.has));
|
|
43
|
+
|
|
44
|
+
const isRecord = (value) => hasKeys(value, RECORD_KEYS) && value.schema === SCHEMA && isPlainObject(value.declined)
|
|
45
|
+
&& Object.entries(value.declined).every(([id, lineage]) => id.length > 0 && typeof lineage === 'string');
|
|
46
|
+
|
|
47
|
+
export const readShippedProfile = (deps = {}) => {
|
|
48
|
+
const read = readRegularFileNoFollow(deps.profilePath ?? PROFILE_PATH, deps);
|
|
49
|
+
if (read.outcome !== OK) return { refusal: 'profile-unreadable' };
|
|
50
|
+
const parsed = parseJson(read.content);
|
|
51
|
+
if (parsed === null || !isProfile(parsed.value)) return { refusal: 'profile-malformed' };
|
|
52
|
+
return { profile: parsed.value };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const readDeclines = (root, deps = {}) => {
|
|
56
|
+
const read = readRegularFileNoFollow(join(root, DECLINES_REL), deps);
|
|
57
|
+
if (read.outcome === ABSENT) return { absent: true };
|
|
58
|
+
if (read.outcome !== OK) {
|
|
59
|
+
return { refusal: read.className === SYMLINK_CLASS ? 'declines-symlink' : 'declines-unreadable' };
|
|
60
|
+
}
|
|
61
|
+
const parsed = parseJson(read.content);
|
|
62
|
+
if (parsed === null || !isRecord(parsed.value)) return { refusal: 'declines-malformed' };
|
|
63
|
+
return { declined: parsed.value.declined };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const isDeclineCurrent = (declined, id, lineage) =>
|
|
67
|
+
isPlainObject(declined) && Object.hasOwn(declined, id) && declined[id] === lineage;
|
|
68
|
+
|
|
69
|
+
refuseDirectRun(import.meta.url);
|