simt-dev 0.6.3 → 0.7.1
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 +162 -841
- package/README.md +21 -135
- package/agents/simt-agent.md +84 -0
- package/bin/simt.mjs +19 -55
- package/docs/decisions/ADR-009-skill-layout.md +1 -1
- package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
- package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +1 -1
- package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
- package/docs/decisions/ADR-106-the-harness-hook-runs-through-a-repo-shim.md +46 -0
- package/docs/decisions/ADR-107-an-install-can-name-one-harness-and-the-skipped-ones-are-named.md +51 -0
- package/docs/decisions/ADR-108-an-adoption-builds-the-index-and-a-command-refreshes-it-on-read.md +59 -0
- package/docs/decisions/ADR-109-simt-dev-is-its-own-consumer-and-the-guards-that-kept-it-from-being-one-are-retired.md +47 -0
- package/docs/decisions/ADR-110-the-lock-file-follows-the-manifest-name-and-the-old-name-is-read-until-the-next-write.md +43 -0
- package/docs/decisions/ADR-111-the-tool-ships-one-agent-rendered-per-harness-and-installs-it-with-init-and-a-command.md +75 -0
- package/docs/decisions/ADR-112-the-registry-is-what-simt-dev-ships-and-a-repo-owns-its-own-skills.md +81 -0
- package/docs/decisions/ADR-113-a-pre-push-that-carries-only-documentation-skips-the-test-gate-and-says-so.md +87 -0
- package/docs/decisions/ADR-114-the-bench-pairs-at-the-task-level-a-missing-cost-is-not-zero-and-an-unasked-cross-group-compare-is-refused.md +74 -0
- package/docs/decisions/ADR-115-the-index-stamps-its-schema-version-migrates-older-databases-in-place-and-serves-newer-ones-from-a-sidecar.md +69 -0
- package/docs/decisions/ADR-116-rust-support-reaches-the-example-path-the-indent-and-the-bench-runner.md +100 -0
- package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
- package/docs/decisions/ADR-118-the-bench-corpus-is-keyed-on-history-is-append-only-and-is-portable-as-a-sealed-document.md +91 -0
- package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
- package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
- package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
- package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
- package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
- package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
- package/package.json +13 -3
- package/rules/core.md +9 -2
- package/rules/mdc/slice-commit.mdc +57 -4
- package/rules/mdc/test-gate.mdc +2 -0
- package/rules/stacks/android.md +3 -3
- package/rules/stacks/connectiq.md +2 -2
- package/rules/stacks/dart.md +1 -1
- package/rules/stacks/deno.md +1 -1
- package/rules/stacks/dotnet.md +1 -1
- package/rules/stacks/elixir.md +1 -1
- package/rules/stacks/go.md +1 -1
- package/rules/stacks/jvm.md +3 -2
- package/rules/stacks/node.md +2 -2
- package/rules/stacks/python.md +7 -6
- package/rules/stacks/ruby.md +1 -1
- package/rules/stacks/rust.md +1 -1
- package/rules/stacks/swift.md +1 -1
- package/rules/stacks/typescript.md +2 -2
- package/rules/stacks/uv.md +3 -2
- package/skills/simt-commands/SKILL.md +70 -0
- package/src/advice/advice.mjs +7 -3
- package/src/agenthooks/decide.mjs +141 -9
- package/src/agenthooks/opencode.mjs +72 -0
- package/src/agenthooks/prompt.mjs +10 -3
- package/src/agenthooks/render.mjs +186 -19
- package/src/agenthooks/shim.mjs +55 -0
- package/src/agents/render.mjs +277 -0
- package/src/api.mjs +29 -0
- package/src/argv.mjs +37 -3
- package/src/bench/ablate.mjs +31 -14
- package/src/bench/adapters.json +16 -0
- package/src/bench/corpus.mjs +150 -0
- package/src/bench/grade.mjs +23 -49
- package/src/bench/mine.mjs +247 -36
- package/src/bench/probes.mjs +104 -68
- package/src/bench/process.mjs +128 -0
- package/src/bench/report.mjs +260 -84
- package/src/bench/run.mjs +251 -150
- package/src/bench/variants.mjs +112 -40
- package/src/brief/assemble.mjs +44 -3
- package/src/ci/workflow.mjs +46 -14
- package/src/cli.mjs +45 -0
- package/src/commands/agent.mjs +205 -0
- package/src/commands/agenthook.mjs +113 -20
- package/src/commands/bench.mjs +192 -18
- package/src/commands/brief.mjs +5 -4
- package/src/commands/ci.mjs +2 -1
- package/src/commands/compare.mjs +50 -49
- package/src/commands/completions.mjs +2 -6
- package/src/commands/decide.mjs +10 -9
- package/src/commands/distill.mjs +4 -3
- package/src/commands/doctor.mjs +11 -7
- package/src/commands/evidence.mjs +19 -12
- package/src/commands/explain.mjs +0 -1
- package/src/commands/gate.mjs +98 -28
- package/src/commands/graph.mjs +20 -8
- package/src/commands/harness.mjs +3 -2
- package/src/commands/help.mjs +160 -31
- package/src/commands/hooks.mjs +163 -50
- package/src/commands/ignore.mjs +2 -1
- package/src/commands/impact.mjs +8 -9
- package/src/commands/index.mjs +278 -180
- package/src/commands/init.mjs +171 -38
- package/src/commands/lease.mjs +206 -39
- package/src/commands/lessons.mjs +72 -15
- package/src/commands/local.mjs +3 -3
- package/src/commands/map.mjs +47 -27
- package/src/commands/mcp.mjs +0 -1
- package/src/commands/pack.mjs +9 -6
- package/src/commands/plan.mjs +6 -5
- package/src/commands/plugin.mjs +3 -3
- package/src/commands/prbody.mjs +3 -3
- package/src/commands/repoindex.mjs +5 -3
- package/src/commands/review.mjs +2 -2
- package/src/commands/rules.mjs +124 -33
- package/src/commands/shared.mjs +2 -3
- package/src/commands/skills.mjs +82 -45
- package/src/commands/skillscompile.mjs +12 -12
- package/src/commands/skillsinstall.mjs +14 -2
- package/src/commands/slice.mjs +396 -26
- package/src/commands/spec.mjs +151 -0
- package/src/commands/status.mjs +4 -7
- package/src/commands/sync.mjs +50 -33
- package/src/commands/tests.mjs +11 -11
- package/src/commands/tokens.mjs +2 -2
- package/src/commands/undo.mjs +29 -31
- package/src/commands/upgrade.mjs +40 -4
- package/src/commands/why.mjs +19 -12
- package/src/compare/hooksstate.mjs +23 -1
- package/src/compare/learn.mjs +82 -57
- package/src/compare/parallel.mjs +50 -0
- package/src/compare/row.mjs +26 -3
- package/src/completions.mjs +5 -0
- package/src/context.mjs +64 -0
- package/src/decisions/dir.mjs +84 -0
- package/src/decisions/index-doc.mjs +46 -0
- package/src/decisions/index.mjs +109 -11
- package/src/diff.mjs +1 -1
- package/src/distill/distill.mjs +17 -2
- package/src/distill/engine.mjs +5 -1
- package/src/ecosystem/node-runner.mjs +144 -0
- package/src/explain.mjs +18 -6
- package/src/graph/load.mjs +42 -20
- package/src/hooks/capability.mjs +17 -4
- package/src/hooks/dispatcher.mjs +513 -36
- package/src/hooks/gates.mjs +0 -43
- package/src/hooks/resolve.mjs +11 -10
- package/src/hooks/shim.mjs +17 -9
- package/src/identity.mjs +96 -15
- package/src/index/extract.mjs +192 -11
- package/src/index/persist.mjs +307 -47
- package/src/index/store.mjs +263 -60
- package/src/index/tests.mjs +6 -35
- package/src/journal.mjs +372 -84
- package/src/kernel/card.mjs +37 -12
- package/src/kernel/checks.mjs +44 -9
- package/src/kernel/variants.mjs +27 -16
- package/src/lease.mjs +97 -15
- package/src/ledger/index.mjs +120 -25
- package/src/local/client.mjs +4 -6
- package/src/locate.mjs +33 -9
- package/src/manifest/schema.mjs +213 -0
- package/src/mcp/resources.mjs +12 -13
- package/src/mcp/server.mjs +151 -38
- package/src/mcp/tools.mjs +90 -16
- package/src/measure/session.mjs +3 -2
- package/src/memory/decide.mjs +98 -43
- package/src/memory/lessons.mjs +168 -33
- package/src/name.mjs +9 -0
- package/src/node-version.mjs +37 -0
- package/src/output/index.mjs +111 -3
- package/src/packs/manage.mjs +17 -3
- package/src/packs.mjs +20 -2
- package/src/plan/index.mjs +31 -8
- package/src/plan/template.mjs +27 -5
- package/src/plugins.mjs +6 -2
- package/src/prbody.mjs +7 -5
- package/src/projection/baselines.mjs +1 -1
- package/src/projection/disclosure.mjs +2 -2
- package/src/projection/loaders.mjs +41 -1
- package/src/projection/lock.mjs +31 -14
- package/src/projection/manifest.mjs +89 -53
- package/src/projection/markers.mjs +43 -3
- package/src/projection/mdc.mjs +34 -7
- package/src/projection/project.mjs +162 -18
- package/src/projection/rules.mjs +58 -6
- package/src/projection/surfaces.mjs +9 -1
- package/src/repo-root.mjs +88 -0
- package/src/resolve/body.mjs +4 -0
- package/src/resolve/fingerprint.mjs +24 -15
- package/src/resolve/local.mjs +58 -0
- package/src/review/critic.mjs +6 -3
- package/src/secrets.mjs +165 -22
- package/src/state.mjs +76 -0
- package/src/status.mjs +9 -4
- package/src/toolwords.mjs +8 -0
- package/src/tsconfig.mjs +39 -99
- package/src/workspace.mjs +55 -10
- package/templates/plan/tracker.json +1 -1
- package/docs/decisions/README.md +0 -591
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// The one answer to "where are this repository's decision records" (review 0003 Q55).
|
|
2
|
+
//
|
|
3
|
+
// Before this, every reader hard-coded `docs/decisions` while detection already
|
|
4
|
+
// knew four spellings (`docs/decisions`, `docs/adr`, `docs/adrs`, `adr`), so a
|
|
5
|
+
// repository that kept its records under `docs/adr/` was fingerprinted correctly
|
|
6
|
+
// and then unread by every command. One resolution, every reader.
|
|
7
|
+
//
|
|
8
|
+
// Precedence is declaration first. A manifest `decisions.dir` names the directory
|
|
9
|
+
// relative to the repository root; without one, the first existing candidate
|
|
10
|
+
// wins; without any, the default is `docs/decisions`. The cutoff below answers
|
|
11
|
+
// the companion question — which records predate structured frontmatter — and it
|
|
12
|
+
// lives in the same config (`decisions.historicalCutoff`), never in code.
|
|
13
|
+
|
|
14
|
+
import { stat } from 'node:fs/promises';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
import { readManifest } from '../projection/manifest.mjs';
|
|
18
|
+
|
|
19
|
+
/** Directory spellings a repository's decision records live under, in priority order. */
|
|
20
|
+
const DECISION_DIR_CANDIDATES = ['docs/decisions', 'docs/adr', 'docs/adrs', 'adr'];
|
|
21
|
+
|
|
22
|
+
/** The default when nothing is declared and nothing exists yet. */
|
|
23
|
+
const DEFAULT_DECISIONS_DIR = 'docs/decisions';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The repository's own `decisions` manifest key, or null.
|
|
27
|
+
*
|
|
28
|
+
* Read with the manifest loader (so the legacy name resolves the same way for
|
|
29
|
+
* this as for every other reader) and returned as-is: validation belongs to
|
|
30
|
+
* the caller that knows what it needs.
|
|
31
|
+
*/
|
|
32
|
+
async function manifestDecisions(root) {
|
|
33
|
+
try {
|
|
34
|
+
const { manifest } = await readManifest(root);
|
|
35
|
+
const decisions = manifest?.decisions;
|
|
36
|
+
return decisions !== null && typeof decisions === 'object' && !Array.isArray(decisions) ? decisions : null;
|
|
37
|
+
} catch {
|
|
38
|
+
// An unreadable manifest is not evidence either way; detection decides.
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function isDir(path) {
|
|
44
|
+
try {
|
|
45
|
+
return (await stat(path)).isDirectory();
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The relative decisions directory for this repository.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} root repository root
|
|
55
|
+
* @returns {Promise<string>} e.g. `docs/decisions`
|
|
56
|
+
*/
|
|
57
|
+
export async function decisionsDir(root) {
|
|
58
|
+
const declared = await manifestDecisions(root);
|
|
59
|
+
if (typeof declared?.dir === 'string' && declared.dir.trim() !== '') {
|
|
60
|
+
return declared.dir.trim().replace(/\\/g, '/').replace(/\/+$/, '');
|
|
61
|
+
}
|
|
62
|
+
for (const candidate of DECISION_DIR_CANDIDATES) {
|
|
63
|
+
if (await isDir(join(root, candidate))) return candidate;
|
|
64
|
+
}
|
|
65
|
+
return DEFAULT_DECISIONS_DIR;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* How many leading records predate structured frontmatter and are exempt from it.
|
|
70
|
+
*
|
|
71
|
+
* The package's own history is the only corpus with such records, and even it
|
|
72
|
+
* declares the number in its own manifest rather than in code: a number in code
|
|
73
|
+
* is a second answer the next record silently moves. Any other repository gets
|
|
74
|
+
* zero unless it declares otherwise.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} root repository root
|
|
77
|
+
* @returns {Promise<number>}
|
|
78
|
+
*/
|
|
79
|
+
export async function decisionsCutoff(root) {
|
|
80
|
+
const declared = await manifestDecisions(root);
|
|
81
|
+
const value = declared?.historicalCutoff;
|
|
82
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) return Math.floor(value);
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// The decisions index document, rendered from the records (review 0003 Q91).
|
|
2
|
+
//
|
|
3
|
+
// The file was 590 hand-written lines: a table of every ADR kept in step by whoever
|
|
4
|
+
// remembered. It agreed with the records until it did not, and nothing read it, so
|
|
5
|
+
// nothing noticed - the failure the generated rule regions exist to prevent, one
|
|
6
|
+
// directory over. The table is output now: this module renders it, `simt rules
|
|
7
|
+
// decisions --yes` writes it, and test/decisions-index.test.mjs fails on drift.
|
|
8
|
+
//
|
|
9
|
+
// The narrative that trailed the table moved to docs/architecture/history.md: prose a
|
|
10
|
+
// reader consults is not prose a command rewrites.
|
|
11
|
+
|
|
12
|
+
const HEADER = `# Decisions
|
|
13
|
+
|
|
14
|
+
Architecture decision records for the SIMT development ecosystem. One canonical home per
|
|
15
|
+
decision; nothing here is duplicated into a repo's \`AGENTS.md\`.
|
|
16
|
+
|
|
17
|
+
A decision is superseded by writing a new ADR with a higher number and editing the old
|
|
18
|
+
record's status line — never by editing the ruling in place.
|
|
19
|
+
|
|
20
|
+
This table is generated from the records: \`simt rules decisions --yes\` writes it, and
|
|
21
|
+
\`test/decisions-index.test.mjs\` fails when it drifts. The history behind the numbers is
|
|
22
|
+
in [architecture/history.md](architecture/history.md).
|
|
23
|
+
|
|
24
|
+
| ADR | Decision | Status |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The index document, from the records.
|
|
30
|
+
*
|
|
31
|
+
* Ordered by number, not by filename: the number IS the order, and a directory listing
|
|
32
|
+
* is not (`ADR-100` sorts before `ADR-99` as text).
|
|
33
|
+
*
|
|
34
|
+
* @param {{id: string, title: string, status: string, file: string}[]} rows
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function renderDecisionsIndex(rows) {
|
|
38
|
+
const ordered = [...rows].sort((a, b) => number(a.id) - number(b.id));
|
|
39
|
+
const lines = ordered.map((row) => `| [${number(row.id)}](decisions/${row.file}) | ${row.title} | ${row.status} |`);
|
|
40
|
+
return `${HEADER}${lines.join('\n')}\n`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `ADR-007` -> `007`, the number the table and the ordering both use. */
|
|
44
|
+
function number(id) {
|
|
45
|
+
return id.replace(/^ADR-/i, '').padStart(3, '0');
|
|
46
|
+
}
|
package/src/decisions/index.mjs
CHANGED
|
@@ -14,13 +14,20 @@
|
|
|
14
14
|
// such, because "no decision behind this" is exactly what a reviewer needs to
|
|
15
15
|
// see and exactly what an empty output would hide.
|
|
16
16
|
|
|
17
|
-
import { readFile, readdir } from 'node:fs/promises';
|
|
17
|
+
import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
|
|
18
18
|
import { readdirSync } from 'node:fs';
|
|
19
19
|
import { dirname, join, resolve } from 'node:path';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
import { MANIFEST_FILE } from '../name.mjs';
|
|
22
22
|
import { CLI } from '../name.mjs';
|
|
23
23
|
import { PREFIX_ARGUING, TOOL_WORDS } from '../toolwords.mjs';
|
|
24
|
+
import { LOCAL_DIR } from '../state.mjs';
|
|
25
|
+
import { packageVersion } from '../package-version.mjs';
|
|
26
|
+
// Q55: read lazily (see commandVerbs). The registry imports the `decide`
|
|
27
|
+
// command, which imports this module, so touching COMMANDS during module
|
|
28
|
+
// evaluation would read an initialising binding on one of the two load
|
|
29
|
+
// orders. Every use below happens at runtime, when both sides are loaded.
|
|
30
|
+
import { COMMANDS } from '../commands/index.mjs';
|
|
24
31
|
import { PARTIAL_CLAUSE, parseDecisionFrontmatter, splitFrontmatter } from '../memory/decide.mjs';
|
|
25
32
|
import { affectsTokens } from '../index/govern.mjs';
|
|
26
33
|
|
|
@@ -82,7 +89,16 @@ export function redactInternalPaths(text) {
|
|
|
82
89
|
return String(text ?? '').replace(internalPathPattern(), 'a file in this tool');
|
|
83
90
|
}
|
|
84
91
|
|
|
85
|
-
|
|
92
|
+
// Q55: four digits and more are real record numbers, not a typo. The old `\d{3}`
|
|
93
|
+
// silently dropped every citation past ADR-999, and lexical sorting put ADR-1000
|
|
94
|
+
// before ADR-999 in every listing that trusted string order.
|
|
95
|
+
const ADR_IN_TEXT = /\bADR-(\d{3,})\b/g;
|
|
96
|
+
|
|
97
|
+
/** Numeric sort for record ids: ADR-1000 follows ADR-999, not ADR-100. */
|
|
98
|
+
function compareAdrIds(a, b) {
|
|
99
|
+
const num = (id) => Number(id.replace(/^ADR-/i, ''));
|
|
100
|
+
return num(a) - num(b) || (a < b ? -1 : a > b ? 1 : 0);
|
|
101
|
+
}
|
|
86
102
|
|
|
87
103
|
/**
|
|
88
104
|
* A line that begins one of the record's other front-matter fields.
|
|
@@ -159,7 +175,7 @@ export function citations(text) {
|
|
|
159
175
|
for (const match of String(text ?? '').matchAll(ADR_IN_TEXT)) {
|
|
160
176
|
found.add(`ADR-${match[1]}`);
|
|
161
177
|
}
|
|
162
|
-
return [...found].sort();
|
|
178
|
+
return [...found].sort(compareAdrIds);
|
|
163
179
|
}
|
|
164
180
|
|
|
165
181
|
/**
|
|
@@ -252,7 +268,11 @@ export function parseDecision(file, text, { all = false } = {}) {
|
|
|
252
268
|
export async function loadDecisions({ all = false, dir = DECISIONS_DIR } = {}) {
|
|
253
269
|
let names;
|
|
254
270
|
try {
|
|
255
|
-
|
|
271
|
+
// Numeric, so the first record `byId` keeps under a duplicate is deterministic
|
|
272
|
+
// (lint flags the collision; the load does not guess between two files).
|
|
273
|
+
names = (await readdir(dir))
|
|
274
|
+
.filter((n) => /^ADR-\d{3,}-.*\.md$/i.test(n))
|
|
275
|
+
.sort((a, b) => compareAdrIds(adrIdOf(a) ?? a, adrIdOf(b) ?? b));
|
|
256
276
|
} catch (error) {
|
|
257
277
|
return { ok: false, decisions: [], errors: [`${dir}: unreadable (${error.message})`] };
|
|
258
278
|
}
|
|
@@ -300,11 +320,72 @@ export async function loadDecisions({ all = false, dir = DECISIONS_DIR } = {}) {
|
|
|
300
320
|
return { ok: errors.length === 0, decisions, errors };
|
|
301
321
|
}
|
|
302
322
|
|
|
323
|
+
/** The record id a filename claims, or null when it claims none. */
|
|
324
|
+
function adrIdOf(file) {
|
|
325
|
+
return /^ADR-(\d{3,})/i.exec(file)?.[0]?.toUpperCase() ?? null;
|
|
326
|
+
}
|
|
327
|
+
|
|
303
328
|
/** Index by id, for a lookup per cited ADR. */
|
|
304
329
|
export function byId(decisions) {
|
|
305
330
|
return new Map(decisions.map((d) => [d.id, d]));
|
|
306
331
|
}
|
|
307
332
|
|
|
333
|
+
/** The command verbs a record may quote, derived from the dispatch registry. */
|
|
334
|
+
export function decisionVerbs() {
|
|
335
|
+
return commandVerbs();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** One file's cache identity: name, size and mtime. Any write changes one of them. */
|
|
339
|
+
async function fileSignature(dir, name) {
|
|
340
|
+
const st = await stat(join(dir, name));
|
|
341
|
+
return { name, size: st.size, mtimeMs: st.mtimeMs };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Load the records, reusing a machine-local cache when the tree is unchanged.
|
|
346
|
+
*
|
|
347
|
+
* Every call used to re-parse every record (`rules why` 44 ms; part of
|
|
348
|
+
* `resources/list` 771 ms). The cache lives under `.simt/local/` — machine
|
|
349
|
+
* state, never committed — and is keyed on the directory, the `all` flag, the
|
|
350
|
+
* running version and one signature per file, so an edit, an add or an upgrade
|
|
351
|
+
* all miss honestly. Best-effort throughout: any cache failure loads fresh.
|
|
352
|
+
*
|
|
353
|
+
* @param {{root: string, dir: string, all?: boolean}} input `dir` is absolute;
|
|
354
|
+
* callers resolve it with `decisionsDir(root)` (or the package directory for
|
|
355
|
+
* shipped records) so this loader never guesses whose records these are.
|
|
356
|
+
*/
|
|
357
|
+
export async function loadDecisionsCached({ root, dir, all = false } = {}) {
|
|
358
|
+
const cachePath = root ? join(root, LOCAL_DIR, 'decisions.index.json') : null;
|
|
359
|
+
let files = null;
|
|
360
|
+
try {
|
|
361
|
+
const names = (await readdir(dir)).filter((n) => /^ADR-\d{3,}-.*\.md$/i.test(n));
|
|
362
|
+
files = [];
|
|
363
|
+
for (const name of names) files.push(await fileSignature(dir, name));
|
|
364
|
+
files.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
365
|
+
} catch {
|
|
366
|
+
return loadDecisions({ all, dir });
|
|
367
|
+
}
|
|
368
|
+
const key = { v: 1, tool: packageVersion(), dir, all, files };
|
|
369
|
+
if (cachePath) {
|
|
370
|
+
try {
|
|
371
|
+
const cached = JSON.parse(await readFile(cachePath, 'utf8'));
|
|
372
|
+
if (JSON.stringify(cached.key) === JSON.stringify(key)) return cached.result;
|
|
373
|
+
} catch {
|
|
374
|
+
// A missing or corrupt cache is a miss, never an error.
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const fresh = await loadDecisions({ all, dir });
|
|
378
|
+
if (cachePath) {
|
|
379
|
+
try {
|
|
380
|
+
await mkdir(join(root, LOCAL_DIR), { recursive: true });
|
|
381
|
+
await writeFile(cachePath, JSON.stringify({ key, result: fresh }), 'utf8');
|
|
382
|
+
} catch {
|
|
383
|
+
// A read-only checkout keeps working uncached.
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return fresh;
|
|
387
|
+
}
|
|
388
|
+
|
|
308
389
|
/**
|
|
309
390
|
* What the decision actually says, in the reader's terms.
|
|
310
391
|
*
|
|
@@ -506,7 +587,7 @@ const section = (name) => {
|
|
|
506
587
|
// A record whose subject is the prefix is arguing rather than citing, so it names none.
|
|
507
588
|
writtenAgainst: PREFIX_ARGUING.has(file)
|
|
508
589
|
? []
|
|
509
|
-
: [...new Set((text.match(ESTATE) ?? []).filter((name) => !TOOL_WORDS.has(name)))].sort(),
|
|
590
|
+
: [...new Set((text.match(ESTATE) ?? []).filter((name) => !TOOL_WORDS.has(name.toLowerCase())))].sort(),
|
|
510
591
|
};
|
|
511
592
|
}
|
|
512
593
|
|
|
@@ -545,8 +626,11 @@ const section = (name) => {
|
|
|
545
626
|
* them - which is what this file used to do. Detection of those now depends on a name
|
|
546
627
|
* never being written into shipped text, rather than on a check.
|
|
547
628
|
*/
|
|
548
|
-
|
|
549
|
-
|
|
629
|
+
// Q55: one shape, two flags. The estate-name pattern used to be written out twice
|
|
630
|
+
// (`ESTATE_NAME` and `ESTATE`), so a shape fixed in one kept matching in the other.
|
|
631
|
+
const ESTATE_SOURCE = '\\b(?:simt-(?!dev\\b)[\\w-]+|repo-\\d{2})\\b';
|
|
632
|
+
|
|
633
|
+
export const ESTATE_NAME = new RegExp(ESTATE_SOURCE);
|
|
550
634
|
|
|
551
635
|
/** Replace estate repository names with a phrase a reader who does not have them can use. */
|
|
552
636
|
export function redactEstate(text) {
|
|
@@ -554,7 +638,7 @@ export function redactEstate(text) {
|
|
|
554
638
|
// The tool's own names are left where they are. Redacting them turned a ruling about
|
|
555
639
|
// `simt-priority` into a sentence about `a repository`, which is how a reader learned
|
|
556
640
|
// that the record had surveyed something and not what it said (ADR-080).
|
|
557
|
-
TOOL_WORDS.has(name) ? name : 'a repository',
|
|
641
|
+
TOOL_WORDS.has(name.toLowerCase()) ? name : 'a repository',
|
|
558
642
|
);
|
|
559
643
|
}
|
|
560
644
|
|
|
@@ -575,10 +659,24 @@ export function redactEstate(text) {
|
|
|
575
659
|
* Only a real verb follows, which is what keeps `simt-dev.json`, `simt-dev-check.yml` and
|
|
576
660
|
* `simt-dev's` untouched - those are the project's artefacts and the project keeps its name.
|
|
577
661
|
*/
|
|
578
|
-
|
|
662
|
+
// Q55: the verbs a record may quote are the commands this binary dispatches,
|
|
663
|
+
// not a hand list. The list used to miss `pack`, `plugin` and `lease`, so a record
|
|
664
|
+
// quoting one read as prose instead of a runnable command.
|
|
665
|
+
let verbAlternation = null;
|
|
666
|
+
function commandVerbs() {
|
|
667
|
+
if (verbAlternation === null) {
|
|
668
|
+
const words = new Set();
|
|
669
|
+
for (const path of COMMANDS) {
|
|
670
|
+
const first = path.split(' ')[0];
|
|
671
|
+
if (first) words.add(first.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
672
|
+
}
|
|
673
|
+
verbAlternation = `(?:${[...words].sort().join('|')}|--[a-z-]+)`;
|
|
674
|
+
}
|
|
675
|
+
return verbAlternation;
|
|
676
|
+
}
|
|
579
677
|
|
|
580
678
|
function redactCommand(text) {
|
|
581
|
-
return String(text ?? '').replace(new RegExp(`\\bsimt-dev (?=${
|
|
679
|
+
return String(text ?? '').replace(new RegExp(`\\bsimt-dev (?=${commandVerbs()})\\b`, 'g'), `${CLI} `);
|
|
582
680
|
}
|
|
583
681
|
|
|
584
682
|
/**
|
|
@@ -590,7 +688,7 @@ function redactCommand(text) {
|
|
|
590
688
|
* enumerating the names here put the portfolio in every adopting repository. The records
|
|
591
689
|
* themselves now carry `repo-NN` labels, so that is what this matches in practice.
|
|
592
690
|
*/
|
|
593
|
-
const ESTATE =
|
|
691
|
+
const ESTATE = new RegExp(ESTATE_SOURCE, 'gi');
|
|
594
692
|
|
|
595
693
|
/** Trim to whole sentences within a budget, never leaving a stub fragment. */
|
|
596
694
|
function sentence(text, limit = 260) {
|
package/src/diff.mjs
CHANGED
|
@@ -49,7 +49,7 @@ export async function readBefore(path) {
|
|
|
49
49
|
export function emitDiffs(pairs, flags) {
|
|
50
50
|
if (isJson(flags)) return;
|
|
51
51
|
const text = renderDiffs(pairs);
|
|
52
|
-
if (text !== '') emit(text
|
|
52
|
+
if (text !== '') emit(text);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/** The `--diff` bytes for a JSON payload: absent entirely without the flag, so
|
package/src/distill/distill.mjs
CHANGED
|
@@ -34,7 +34,10 @@ import {
|
|
|
34
34
|
} from './engine.mjs';
|
|
35
35
|
|
|
36
36
|
export const DEFAULT_BUDGET = 2048;
|
|
37
|
-
|
|
37
|
+
import { LOCAL_DIR } from '../state.mjs';
|
|
38
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
39
|
+
|
|
40
|
+
const DISTILL_DIR = join(LOCAL_DIR, 'distilled');
|
|
38
41
|
|
|
39
42
|
/**
|
|
40
43
|
* The row's acceptance metric, stated where a caller reads results (ADR-076):
|
|
@@ -173,7 +176,7 @@ export async function distillTarget(args, flags) {
|
|
|
173
176
|
);
|
|
174
177
|
}
|
|
175
178
|
|
|
176
|
-
const root = resolve(flags
|
|
179
|
+
const root = resolve(commandRoot(flags));
|
|
177
180
|
const source = await resolveTarget(target, root, flags);
|
|
178
181
|
if (source.exit !== undefined) return source.exit;
|
|
179
182
|
|
|
@@ -190,6 +193,16 @@ export async function distillTarget(args, flags) {
|
|
|
190
193
|
const cached = flags.refresh === true ? null : await readFile(absProposal, 'utf8').catch(() => null);
|
|
191
194
|
if (cached !== null) {
|
|
192
195
|
const fields = parseExistingFrontmatter(cached);
|
|
196
|
+
let faults = [];
|
|
197
|
+
if (typeof fields.faults === 'string') {
|
|
198
|
+
try {
|
|
199
|
+
const parsedFaults = JSON.parse(fields.faults);
|
|
200
|
+
if (Array.isArray(parsedFaults)) faults = parsedFaults.map(String);
|
|
201
|
+
} catch {
|
|
202
|
+
// A malformed faults line is not a reason to refuse the proposal; the proposal
|
|
203
|
+
// still says it is unverified, and `--refresh` re-derives the list.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
193
206
|
return {
|
|
194
207
|
ok: true,
|
|
195
208
|
command: 'distill',
|
|
@@ -199,6 +212,7 @@ export async function distillTarget(args, flags) {
|
|
|
199
212
|
budget: Number(fields.budget ?? budget),
|
|
200
213
|
citations: countCitations(cached),
|
|
201
214
|
verified: fields.verified !== 'false',
|
|
215
|
+
faults,
|
|
202
216
|
metric: { note: METRIC_NOTE },
|
|
203
217
|
proposal: { path: relProposal, state: 'cached', bytes: Buffer.byteLength(cached, 'utf8') },
|
|
204
218
|
};
|
|
@@ -225,6 +239,7 @@ export async function distillTarget(args, flags) {
|
|
|
225
239
|
engine: provenance,
|
|
226
240
|
budget,
|
|
227
241
|
verified: faults.length === 0,
|
|
242
|
+
faults,
|
|
228
243
|
}) + `\n${outcome.body}\n`;
|
|
229
244
|
|
|
230
245
|
await mkdir(join(root, DISTILL_DIR), { recursive: true });
|
package/src/distill/engine.mjs
CHANGED
|
@@ -167,7 +167,7 @@ export function verifyCitations(body, sourceLineCount) {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
/** The proposal file's frontmatter. No wall-clock value here: byte-stable (P4). */
|
|
170
|
-
export function proposalFrontmatter({ source, digest, engine, budget, prompt, verified, verdict, advisory }) {
|
|
170
|
+
export function proposalFrontmatter({ source, digest, engine, budget, prompt, verified, verdict, advisory, faults = [] }) {
|
|
171
171
|
return [
|
|
172
172
|
'---',
|
|
173
173
|
`source: ${source}`,
|
|
@@ -178,6 +178,10 @@ export function proposalFrontmatter({ source, digest, engine, budget, prompt, ve
|
|
|
178
178
|
verdict ? `verdict: ${verdict}` : null,
|
|
179
179
|
advisory ? 'advisory: true' : null,
|
|
180
180
|
verified === false ? 'verified: false' : 'verified: true',
|
|
181
|
+
// The faults belong to the proposal, not only to the report that wrote it: a cached
|
|
182
|
+
// proposal re-read without them reported `verified:false` and nothing to fix
|
|
183
|
+
// (review 0003 Q52).
|
|
184
|
+
Array.isArray(faults) && faults.length > 0 ? `faults: ${JSON.stringify(faults)}` : null,
|
|
181
185
|
'status: proposal',
|
|
182
186
|
'---',
|
|
183
187
|
'',
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// The one Node runner resolver every consumer reads (review 0003 Q31).
|
|
2
|
+
//
|
|
3
|
+
// Before this, four tables answered "how does this repo run a script" and
|
|
4
|
+
// disagreed: the card printed `npm run`, the dispatcher spawned `npm run`,
|
|
5
|
+
// the manifest remedies named `npm run`, and CI installed with `npm ci` — in a
|
|
6
|
+
// repository whose lockfile and `packageManager` said pnpm. A pnpm adoption
|
|
7
|
+
// then failed on its first pull request, and a docs-only or Go repo got
|
|
8
|
+
// remedies proposing npm gates it could never run.
|
|
9
|
+
//
|
|
10
|
+
// One data table, one resolver, every consumer. The table is records, not
|
|
11
|
+
// code, so the Q70 ecosystem work can adopt it without a second answer: a
|
|
12
|
+
// future `ecosystems/<id>.json` row carries the same fields and this resolver
|
|
13
|
+
// stays the shape of the lookup.
|
|
14
|
+
//
|
|
15
|
+
// Precedence is committed evidence first. A lockfile on disk names the manager
|
|
16
|
+
// the repo actually installs with; `packageManager` names the one it declares.
|
|
17
|
+
// Where the two disagree `checkToolchain` already reports `mismatched`, and the
|
|
18
|
+
// runner follows the lockfile — the install step must restore the tree CI
|
|
19
|
+
// checks out, not the field. With no lockfile the declared field wins over the
|
|
20
|
+
// npm default, so a fresh `pnpm init` names pnpm before its first install.
|
|
21
|
+
|
|
22
|
+
export const NODE_MANAGERS = [
|
|
23
|
+
{
|
|
24
|
+
id: 'pnpm',
|
|
25
|
+
lockfiles: ['pnpm-lock.yaml'],
|
|
26
|
+
fact: 'has-pnpm-lock',
|
|
27
|
+
run: 'pnpm',
|
|
28
|
+
install: 'pnpm install --frozen-lockfile',
|
|
29
|
+
installFallback: 'pnpm install',
|
|
30
|
+
addDev: 'pnpm add -D',
|
|
31
|
+
silent: true,
|
|
32
|
+
setupNode: true,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'yarn',
|
|
36
|
+
lockfiles: ['yarn.lock'],
|
|
37
|
+
fact: 'has-yarn-lock',
|
|
38
|
+
run: 'yarn',
|
|
39
|
+
install: 'yarn install --frozen-lockfile',
|
|
40
|
+
installFallback: 'yarn install',
|
|
41
|
+
addDev: 'yarn add -D',
|
|
42
|
+
silent: true,
|
|
43
|
+
setupNode: true,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'bun',
|
|
47
|
+
lockfiles: ['bun.lock', 'bun.lockb'],
|
|
48
|
+
fact: 'has-bun-lock',
|
|
49
|
+
run: 'bun',
|
|
50
|
+
install: 'bun install --frozen-lockfile',
|
|
51
|
+
installFallback: 'bun install',
|
|
52
|
+
addDev: 'bun add -d',
|
|
53
|
+
silent: false,
|
|
54
|
+
setupNode: false,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'npm',
|
|
58
|
+
lockfiles: ['package-lock.json'],
|
|
59
|
+
fact: 'has-package-lock',
|
|
60
|
+
run: 'npm',
|
|
61
|
+
install: 'npm ci',
|
|
62
|
+
installFallback: 'npm install',
|
|
63
|
+
addDev: 'npm i -D',
|
|
64
|
+
silent: true,
|
|
65
|
+
setupNode: true,
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const BY_ID = new Map(NODE_MANAGERS.map((row) => [row.id, row]));
|
|
70
|
+
const BY_LOCKFILE = new Map();
|
|
71
|
+
for (const row of NODE_MANAGERS) {
|
|
72
|
+
for (const file of row.lockfiles) BY_LOCKFILE.set(file, row);
|
|
73
|
+
}
|
|
74
|
+
const BY_FACT = new Map(NODE_MANAGERS.map((row) => [row.fact, row]));
|
|
75
|
+
|
|
76
|
+
/** The npm row. Exported so callers never spell a default twice. */
|
|
77
|
+
export function npmManager() {
|
|
78
|
+
return BY_ID.get('npm');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Name the manager a `packageManager` field declares, or null.
|
|
83
|
+
*
|
|
84
|
+
* `packageManager` is `manager@version`; only the manager names the runner.
|
|
85
|
+
* Unknown managers and non-strings are null, never a guess.
|
|
86
|
+
*/
|
|
87
|
+
export function managerFromPackageField(value) {
|
|
88
|
+
if (typeof value !== 'string') return null;
|
|
89
|
+
const id = value.split('@')[0].trim();
|
|
90
|
+
return BY_ID.has(id) ? id : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolve the one Node runner for a repository.
|
|
95
|
+
*
|
|
96
|
+
* @param {{facts?: string[]|Set<string>, lockfiles?: string[], packageManager?: string|null}} input
|
|
97
|
+
* @returns {object} the NODE_MANAGERS row that owns this repo
|
|
98
|
+
*/
|
|
99
|
+
export function resolveNodeRunner({ facts = [], lockfiles = [], packageManager = null } = {}) {
|
|
100
|
+
const present = facts instanceof Set ? facts : new Set(facts);
|
|
101
|
+
for (const row of NODE_MANAGERS) {
|
|
102
|
+
if (present.has(row.fact)) return row;
|
|
103
|
+
for (const file of row.lockfiles) {
|
|
104
|
+
if (lockfiles.includes(file)) return row;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const declared = managerFromPackageField(packageManager);
|
|
108
|
+
if (declared) return BY_ID.get(declared);
|
|
109
|
+
return npmManager();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Whether this runner id spells gates as `<run> run <script>`. */
|
|
113
|
+
export function isNodeRunner(runner) {
|
|
114
|
+
return BY_ID.has(runner);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The command that runs one script: `pnpm run lint`, `bun run lint`. */
|
|
118
|
+
export function runScript(manager, script) {
|
|
119
|
+
const id = typeof manager === 'string' ? manager : manager?.id ?? 'npm';
|
|
120
|
+
const run = BY_ID.get(id)?.run ?? 'npm';
|
|
121
|
+
return `${run} run ${script}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The argv the dispatcher spawns for one script gate. */
|
|
125
|
+
export function runScriptArgv(manager, script) {
|
|
126
|
+
const id = typeof manager === 'string' ? manager : manager?.id ?? 'npm';
|
|
127
|
+
const row = BY_ID.get(id) ?? npmManager();
|
|
128
|
+
if (row.silent) return [row.run, 'run', '--silent', script];
|
|
129
|
+
return [row.run, 'run', script];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Install step for CI: the frozen command plus its fallback. */
|
|
133
|
+
export function installCommands(manager) {
|
|
134
|
+
const row = typeof manager === 'string' ? (BY_ID.get(manager) ?? npmManager()) : (manager ?? npmManager());
|
|
135
|
+
return { install: row.install, fallback: row.installFallback, setupNode: row.setupNode };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Look up a manager record by lockfile name, fact, or id. */
|
|
139
|
+
export function lookupNodeManager({ lockfile = null, fact = null, id = null } = {}) {
|
|
140
|
+
if (id && BY_ID.has(id)) return BY_ID.get(id);
|
|
141
|
+
if (lockfile && BY_LOCKFILE.has(lockfile)) return BY_LOCKFILE.get(lockfile);
|
|
142
|
+
if (fact && BY_FACT.has(fact)) return BY_FACT.get(fact);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
package/src/explain.mjs
CHANGED
|
@@ -59,8 +59,8 @@ export const EXPLAIN_CODES = {
|
|
|
59
59
|
},
|
|
60
60
|
'file-lines': {
|
|
61
61
|
what: 'A file exceeds the 1000-line hard ceiling: it fails the gate.',
|
|
62
|
-
why: 'ADR-005: 400 lines is a review trigger, 1000 is a hard ceiling, and
|
|
63
|
-
fix: 'Split the file, or
|
|
62
|
+
why: 'ADR-005: 400 lines is a review trigger, 1000 is a hard ceiling, and excluded files are exempt. The ceiling is a default a repository can narrow, never a number an agent negotiates at write time.',
|
|
63
|
+
fix: 'Split the file, or name it in limits.fileLines.exclude — not by editing the generated line.',
|
|
64
64
|
example: 'A 1240-line module with no exemption: FILE-LINES names the file and the count.',
|
|
65
65
|
},
|
|
66
66
|
'file-lines-warn': {
|
|
@@ -94,10 +94,10 @@ export const EXPLAIN_CODES = {
|
|
|
94
94
|
example: 'gates.cwd.test set to "../shared" in a monorepo member.',
|
|
95
95
|
},
|
|
96
96
|
inconsistent: {
|
|
97
|
-
what: 'The
|
|
98
|
-
why: '
|
|
99
|
-
fix: '
|
|
100
|
-
example: '
|
|
97
|
+
what: 'The project\u2019s tsconfig spells `moduleResolution` in a form the TypeScript parser accepts but the documentation does not, so it compiles and quietly disagrees with every repository that spells it the documented way.',
|
|
98
|
+
why: 'The parser is case-insensitive, which is the whole reason a workspace drifts on this key: both spellings work, so nothing ever said the other one did not. Consistency with the documentation is the only spelling checkable in one repository at a time \u2014 consistency with the other repositories would need a workspace, and `check` runs in a repository on its own.',
|
|
99
|
+
fix: 'Use the documented spelling the finding names; it compiles identically.',
|
|
100
|
+
example: '"moduleResolution": "Bundler" where the documentation writes "bundler".',
|
|
101
101
|
},
|
|
102
102
|
inert: {
|
|
103
103
|
what: 'The TypeScript base config is present but inert: nothing in the repository reads it, so it constrains no file.',
|
|
@@ -117,6 +117,12 @@ export const EXPLAIN_CODES = {
|
|
|
117
117
|
fix: 'Fix the simt-supersedes entry to [{ adr: "ADR-NNN", ruling: N }], with a real record and ruling number.',
|
|
118
118
|
example: 'simt-supersedes: [{ adr: "ADR-999", ruling: 1 }] when no ADR-999 exists.',
|
|
119
119
|
},
|
|
120
|
+
'duplicate-decision-id': {
|
|
121
|
+
what: 'Two decision records claim one number: the index only sees the first, so the second is a record no provenance answer can reach.',
|
|
122
|
+
why: 'Review 0003 Q55: byId keeps one silently, and a reader of the other gets a decision the tooling cannot resolve. Failing names both files while the collision is still a rename, not a rewrite of history.',
|
|
123
|
+
fix: 'Renumber the newer record to a free number, then rerun the check.',
|
|
124
|
+
example: 'ADR-002-first.md and ADR-002-second.md: DUPLICATE-DECISION-ID names both.',
|
|
125
|
+
},
|
|
120
126
|
'lock-absent': {
|
|
121
127
|
what: 'No lock is recorded: the projection was never locked, so there is nothing `--frozen` can compare against.',
|
|
122
128
|
why: 'ADR-086: the lock is the byte-identical contract across machines, and `--frozen` without one would compare the projection against nothing and call it current. Reported only under --frozen, because a repository that never locked is not drift.',
|
|
@@ -243,6 +249,12 @@ export const EXPLAIN_CODES = {
|
|
|
243
249
|
fix: 'Read the detail (it carries the OS error): fix permissions, remove the broken link, then re-run.',
|
|
244
250
|
example: 'AGENTS.md unreadable (EACCES): UNREADABLE with the errno, not ABSENT.',
|
|
245
251
|
},
|
|
252
|
+
unresolvable: {
|
|
253
|
+
what: 'The checked-in gate workflow cannot resolve simt-dev by any path its locator tries: no pinned devDependency, no release binary, no simt on PATH.',
|
|
254
|
+
why: 'Review 0003 Q31: a green tick from a check that never ran is the one outcome worse than a red one. The workflow installs whatever the repo pins, so a repo that pins nothing gates nothing — reported here instead of failing open in CI.',
|
|
255
|
+
fix: 'Run the install the finding names (the repo manager\u2019s add-dev command) so CI restores the pinned dependency.',
|
|
256
|
+
example: 'A pnpm repo whose workflow says npm ci with no simt-dev dependency: UNRESOLVABLE names pnpm add -D.',
|
|
257
|
+
},
|
|
246
258
|
unrunnable: {
|
|
247
259
|
what: 'A declared gate cannot run here: the command it needs is not on PATH, or its runner is missing.',
|
|
248
260
|
why: 'ADR-017: a gate that cannot run fails closed on every commit once hooks are installed — after the reader committed, which is the wrong order. check reports it before anyone installs anything, with the probe result that proved it.',
|