simt-dev 0.7.0 → 0.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 +191 -870
- package/README.md +188 -213
- package/bin/simt.mjs +19 -55
- package/docs/decisions/ADR-014-workspace-is-configuration.md +2 -2
- package/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +1 -1
- package/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +1 -1
- package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +1 -1
- package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +1 -1
- package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +1 -1
- package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +1 -1
- package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +1 -1
- package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.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-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/docs/decisions/ADR-126-the-mcp-surface-stays-a-reader-so-there-is-no-write-path-to-build.md +68 -0
- package/docs/decisions/ADR-127-the-mcp-tool-list-is-written-by-hand-and-roots-is-not-asked-for.md +65 -0
- package/docs/decisions/ADR-128-a-research-plan-is-a-plan-and-the-tracker-reads-it.md +42 -0
- package/ecosystems/bun.json +6 -0
- package/ecosystems/cargo.json +25 -0
- package/ecosystems/composer.json +6 -0
- package/ecosystems/dart.json +24 -0
- package/ecosystems/deno.json +21 -0
- package/ecosystems/devcontainers.json +6 -0
- package/ecosystems/docker-compose.json +6 -0
- package/ecosystems/docker.json +6 -0
- package/ecosystems/dotnet.json +25 -0
- package/ecosystems/elixir.json +23 -0
- package/ecosystems/gitsubmodule.json +6 -0
- package/ecosystems/go.json +25 -0
- package/ecosystems/gradle.json +13 -0
- package/ecosystems/maven.json +23 -0
- package/ecosystems/node.json +25 -0
- package/ecosystems/python.json +20 -0
- package/ecosystems/ruby.json +23 -0
- package/ecosystems/swift.json +25 -0
- package/ecosystems/terraform.json +6 -0
- package/ecosystems/uv.json +6 -0
- package/harnesses/antigravity.json +162 -0
- package/harnesses/claude.json +206 -0
- package/harnesses/codex.json +136 -0
- package/harnesses/copilot.json +88 -0
- package/harnesses/cursor.json +140 -0
- package/harnesses/deepseek.json +99 -0
- package/harnesses/gemini.json +122 -0
- package/harnesses/opencode.json +173 -0
- package/package.json +12 -2
- package/rules/core.md +7 -0
- package/rules/mdc/intake-questions.mdc +8 -0
- package/rules/mdc/slice-commit.mdc +57 -4
- package/src/advice/advice.mjs +3 -2
- package/src/agenthooks/prompt.mjs +5 -2
- package/src/agenthooks/render.mjs +86 -180
- package/src/agenthooks/shim.mjs +1 -1
- package/src/agents/render.mjs +15 -18
- package/src/api.mjs +31 -0
- package/src/argv.mjs +48 -3
- package/src/bench/ablate.mjs +3 -4
- package/src/bench/adapters.json +163 -105
- package/src/bench/adapters.mjs +466 -0
- package/src/bench/arms.mjs +333 -0
- package/src/bench/grade.mjs +33 -73
- package/src/bench/graders.mjs +42 -0
- package/src/bench/mine.mjs +121 -475
- package/src/bench/probe-ledger.mjs +149 -0
- package/src/bench/probes.catalog.mjs +918 -0
- package/src/bench/probes.mjs +79 -1064
- package/src/bench/render.mjs +111 -0
- package/src/bench/report.mjs +120 -962
- package/src/bench/run.mjs +185 -802
- package/src/bench/variants.mjs +4 -5
- package/src/bench/workspace.mjs +160 -0
- package/src/brief/assemble.mjs +48 -15
- package/src/ci/dependabot.mjs +25 -55
- package/src/ci/workflow.mjs +30 -7
- package/src/cli.mjs +45 -0
- package/src/commands/agent.mjs +4 -13
- package/src/commands/agenthook.mjs +70 -40
- package/src/commands/bench.mjs +191 -18
- package/src/commands/brief.mjs +5 -4
- package/src/commands/ci.mjs +13 -14
- package/src/commands/compare.mjs +42 -42
- package/src/commands/completions.mjs +2 -6
- package/src/commands/decide.mjs +4 -4
- package/src/commands/distill.mjs +4 -3
- package/src/commands/docs.mjs +88 -0
- package/src/commands/doctor.mjs +11 -7
- package/src/commands/evidence.mjs +6 -9
- package/src/commands/explain.mjs +0 -1
- package/src/commands/gate.mjs +26 -25
- package/src/commands/graph.mjs +2 -3
- package/src/commands/harness.mjs +6 -5
- package/src/commands/help.mjs +168 -19
- package/src/commands/hooks.mjs +119 -49
- package/src/commands/ignore.mjs +13 -8
- package/src/commands/impact.mjs +8 -15
- package/src/commands/index.mjs +318 -193
- package/src/commands/init-scan.mjs +90 -0
- package/src/commands/init.mjs +124 -105
- package/src/commands/lease.mjs +73 -25
- package/src/commands/lessons.mjs +10 -9
- package/src/commands/local.mjs +3 -3
- package/src/commands/map.mjs +24 -14
- package/src/commands/mcp.mjs +2 -18
- package/src/commands/pack.mjs +9 -6
- package/src/commands/plan.mjs +6 -5
- package/src/commands/plugin.mjs +2 -2
- package/src/commands/prbody.mjs +3 -3
- package/src/commands/repoindex.mjs +12 -11
- package/src/commands/review.mjs +2 -2
- package/src/commands/rules.mjs +37 -16
- package/src/commands/shared.mjs +14 -19
- package/src/commands/skills.mjs +33 -32
- package/src/commands/skillscompile.mjs +7 -8
- package/src/commands/skillsinstall.mjs +7 -6
- package/src/commands/slice.mjs +220 -74
- package/src/commands/spec.mjs +167 -0
- package/src/commands/status.mjs +27 -18
- package/src/commands/sync.mjs +27 -25
- package/src/commands/tests.mjs +18 -21
- package/src/commands/tokens.mjs +21 -26
- package/src/commands/undo.mjs +5 -15
- package/src/commands/upgrade.mjs +15 -7
- package/src/commands/why.mjs +15 -21
- package/src/compare/hooksstate.mjs +2 -6
- package/src/compare/learn.mjs +3 -1
- package/src/compare/row.mjs +0 -2
- package/src/context.mjs +64 -0
- package/src/decisions/index-doc.mjs +46 -0
- package/src/decisions/index.mjs +3 -2
- package/src/diff.mjs +1 -1
- package/src/distill/distill.mjs +14 -1
- package/src/distill/engine.mjs +5 -1
- package/src/docs/commands.mjs +65 -0
- package/src/ecosystem/records.mjs +219 -0
- package/src/explain.mjs +31 -0
- package/src/gates/index.mjs +385 -0
- package/src/graph/compile.mjs +8 -4
- package/src/graph/load.mjs +6 -2
- package/src/graph/spec.mjs +10 -8
- package/src/harness/records.mjs +143 -0
- package/src/hooks/capability.mjs +7 -5
- package/src/hooks/dispatcher.mjs +209 -60
- package/src/hooks/gates.mjs +10 -43
- package/src/hooks/resolve.mjs +4 -14
- package/src/hooks/shim.mjs +5 -2
- package/src/hooks/where.mjs +2 -7
- package/src/identity.mjs +31 -12
- package/src/index/coverage.mjs +58 -0
- package/src/index/extract.mjs +6 -16
- package/src/index/gate.mjs +1 -1
- package/src/index/govern.mjs +26 -9
- package/src/index/impact.mjs +4 -8
- package/src/index/map.mjs +14 -15
- package/src/index/open.mjs +173 -0
- package/src/index/persist.mjs +224 -57
- package/src/index/store.mjs +82 -39
- package/src/index/tests.mjs +2 -6
- package/src/journal.mjs +94 -20
- package/src/kernel/card.mjs +37 -63
- package/src/kernel/checks.mjs +4 -10
- package/src/lease.mjs +102 -7
- package/src/ledger/index.mjs +150 -14
- package/src/local/client.mjs +4 -6
- package/src/locate.mjs +33 -9
- package/src/manifest/schema.mjs +213 -0
- package/src/mcp/paginate.mjs +33 -0
- package/src/mcp/resources.mjs +7 -10
- package/src/mcp/schemas.mjs +512 -0
- package/src/mcp/server.mjs +119 -25
- package/src/mcp/tools-facts.mjs +326 -0
- package/src/mcp/tools-read.mjs +221 -0
- package/src/mcp/tools.mjs +4 -967
- package/src/measure/corpus.mjs +150 -0
- package/src/measure/metrics.mjs +236 -0
- package/src/measure/records.mjs +211 -0
- package/src/measure/session.mjs +3 -2
- package/src/measure/stats.mjs +538 -0
- package/src/measure/task-schema.mjs +125 -0
- package/src/measure/tasks.mjs +69 -0
- package/src/memory/decide.mjs +6 -15
- package/src/memory/lesson-text.mjs +61 -0
- package/src/memory/lessons.mjs +29 -82
- package/src/name.mjs +7 -0
- package/src/node-version.mjs +37 -0
- package/src/output/index.mjs +209 -10
- package/src/packs/manage.mjs +32 -12
- package/src/packs.mjs +20 -2
- package/src/plan/index.mjs +1 -1
- package/src/plugins.mjs +22 -16
- package/src/prbody.mjs +7 -11
- package/src/projection/loaders.mjs +7 -692
- package/src/projection/lock.mjs +11 -1
- package/src/projection/manifest.mjs +29 -468
- package/src/projection/mdc.mjs +7 -8
- package/src/projection/project.mjs +126 -221
- package/src/projection/region.mjs +561 -0
- package/src/projection/rules.mjs +20 -550
- package/src/projection/surfaces.mjs +43 -210
- package/src/projection/targets.mjs +5 -5
- package/src/repo-root.mjs +31 -0
- package/src/resolve/eval.mjs +2 -1
- package/src/resolve/fingerprint.mjs +19 -22
- package/src/resolve/resolve.mjs +1 -41
- package/src/review/critic.mjs +10 -13
- package/src/run.mjs +98 -0
- package/src/secrets.mjs +14 -22
- package/src/state/events.mjs +129 -0
- package/src/status-snapshot.mjs +233 -0
- package/src/status.mjs +2 -6
- package/src/sys/git.mjs +82 -0
- package/src/sys/proc.mjs +151 -0
- package/src/testing/parsers.mjs +265 -0
- package/src/testing/patches.mjs +23 -0
- package/src/testing/paths.mjs +49 -0
- package/src/testing/runners.mjs +186 -0
- package/src/testing/vault.mjs +68 -0
- package/src/text/frontmatter.mjs +49 -0
- package/src/text/tokens.mjs +52 -0
- package/src/toolwords.mjs +3 -0
- package/src/util/fs.mjs +20 -0
- package/src/util/sort.mjs +12 -0
- package/src/workspace.mjs +58 -11
- package/src/write/plan.mjs +82 -0
- package/templates/plan/tracker.json +1 -0
- package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +0 -82
- package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +0 -49
- package/docs/decisions/README.md +0 -600
- package/src/bench/tasks.mjs +0 -36
|
@@ -29,9 +29,9 @@ import { diff, parse, inject, BEGIN, END } from './markers.mjs';
|
|
|
29
29
|
import { budgetFinding, readManifest } from './manifest.mjs';
|
|
30
30
|
import { budgetCutFinding } from '../kernel/budget.mjs';
|
|
31
31
|
import { measureRoot } from '../measure/context.mjs';
|
|
32
|
-
import { loadMdcRules
|
|
32
|
+
import { loadMdcRules } from './mdc.mjs';
|
|
33
33
|
import { foldsRules, resolveSurfaces, SURFACES, SURFACE_IDS } from './surfaces.mjs';
|
|
34
|
-
import { pointerState, pointerTargets,
|
|
34
|
+
import { pointerState, pointerTargets, scopedTargets } from './targets.mjs';
|
|
35
35
|
import { CLI, LOCK_FILE, LOCK_LEGACY, PACKAGE_NAME } from '../name.mjs';
|
|
36
36
|
import { MANIFEST_FILE, WORKFLOW_FILE_NAME } from '../name.mjs';
|
|
37
37
|
import { resolveNodeRunner } from '../ecosystem/node-runner.mjs';
|
|
@@ -101,21 +101,7 @@ async function workflowResolvable(root) {
|
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
103
|
import { computeLock } from './lock.mjs';
|
|
104
|
-
import {
|
|
105
|
-
desiredRegion,
|
|
106
|
-
provenanceDigest,
|
|
107
|
-
isEligible,
|
|
108
|
-
loadRules,
|
|
109
|
-
packageRegions,
|
|
110
|
-
LOCAL_PROFILE_DIR,
|
|
111
|
-
mdcTargets,
|
|
112
|
-
readDeclared,
|
|
113
|
-
ruleValues,
|
|
114
|
-
render,
|
|
115
|
-
ROOT,
|
|
116
|
-
select,
|
|
117
|
-
SUBSTITUTIONS_KEY,
|
|
118
|
-
} from './rules.mjs';
|
|
104
|
+
import { desiredRegion, provenanceDigest, isEligible, loadRules, packageRegions, LOCAL_PROFILE_DIR, mdcTargets, readDeclared, render, select, SUBSTITUTIONS_KEY } from './rules.mjs';
|
|
119
105
|
|
|
120
106
|
/** The one file every repo gets. Chosen deliberately: AGENTS.md is read by the
|
|
121
107
|
* most agents, including ones that ignore .cursor/ entirely. */
|
|
@@ -131,7 +117,6 @@ function regionVerdict(action) {
|
|
|
131
117
|
: { status: 'drift', detail: `region is ${action}; run: ${CLI} sync --yes` };
|
|
132
118
|
}
|
|
133
119
|
|
|
134
|
-
|
|
135
120
|
/**
|
|
136
121
|
* @param {string} root repo root
|
|
137
122
|
* @returns {Promise<{root:string, stack:string[], facts:string[], profiles:string[],
|
|
@@ -236,10 +221,12 @@ async function targetFor(root, dir, body, meta, label = PRIMARY_TARGET) {
|
|
|
236
221
|
const path = join(dir, PRIMARY_TARGET);
|
|
237
222
|
let existing = '';
|
|
238
223
|
let exists = true;
|
|
224
|
+
let readError = null;
|
|
239
225
|
try {
|
|
240
226
|
existing = await readFile(path, 'utf8');
|
|
241
|
-
} catch {
|
|
227
|
+
} catch (error) {
|
|
242
228
|
exists = false;
|
|
229
|
+
readError = error.code ?? String(error);
|
|
243
230
|
}
|
|
244
231
|
|
|
245
232
|
const result = inject(existing, body, meta, await regionOpts(root));
|
|
@@ -248,6 +235,7 @@ async function targetFor(root, dir, body, meta, label = PRIMARY_TARGET) {
|
|
|
248
235
|
// document once per package, and a plan that printed `AGENTS.md` twice with no
|
|
249
236
|
// directory on either line would leave the reader unable to tell which is which.
|
|
250
237
|
file: label,
|
|
238
|
+
kind: 'region',
|
|
251
239
|
path,
|
|
252
240
|
exists,
|
|
253
241
|
action: result.action,
|
|
@@ -261,6 +249,7 @@ async function targetFor(root, dir, body, meta, label = PRIMARY_TARGET) {
|
|
|
261
249
|
line: result.line,
|
|
262
250
|
bytes: Buffer.byteLength(result.text),
|
|
263
251
|
previousBytes: Buffer.byteLength(existing),
|
|
252
|
+
readError,
|
|
264
253
|
sha: meta.sha,
|
|
265
254
|
body,
|
|
266
255
|
text: result.text,
|
|
@@ -268,33 +257,123 @@ async function targetFor(root, dir, body, meta, label = PRIMARY_TARGET) {
|
|
|
268
257
|
};
|
|
269
258
|
}
|
|
270
259
|
|
|
260
|
+
/**
|
|
261
|
+
* The findings one write target produces, read off the target itself.
|
|
262
|
+
*
|
|
263
|
+
* This is what makes "the gate equals the writer" true by construction rather than by
|
|
264
|
+
* two implementations happening to agree. `plan` builds every file this tool writes,
|
|
265
|
+
* each through the same `inject` call; the gate asks each target what it would have
|
|
266
|
+
* done instead of rebuilding the file and comparing. The version this replaced rebuilt
|
|
267
|
+
* the region itself with a second `diff` call, and a second construction is invisible
|
|
268
|
+
* while it agrees — the only way to notice is the disagreement some future edit
|
|
269
|
+
* produces, reported as drift on a tree `sync` has just written.
|
|
270
|
+
*
|
|
271
|
+
* Returns an array: a baseline whose region has moved down the file is both current and
|
|
272
|
+
* misplaced, and both findings name the same file.
|
|
273
|
+
*
|
|
274
|
+
* @param {object} target one entry from `plan(root).targets`
|
|
275
|
+
* @returns {object[]} findings, in the order they should be printed
|
|
276
|
+
*/
|
|
277
|
+
function targetFindings(target) {
|
|
278
|
+
if (target === undefined) return [];
|
|
279
|
+
// The lock is written here and compared by the frozen check, not reported as drift;
|
|
280
|
+
// a pointer is a symlink the writer makes, reported by its own loop below.
|
|
281
|
+
if (target.kind === 'lock' || target.kind === 'pointer') return [];
|
|
282
|
+
|
|
283
|
+
if (target.kind === 'baseline') {
|
|
284
|
+
if (target.action === 'malformed') {
|
|
285
|
+
return [{ file: target.file, status: 'malformed', detail: target.reason, line: target.line }];
|
|
286
|
+
}
|
|
287
|
+
if (!target.exists) {
|
|
288
|
+
return [
|
|
289
|
+
{
|
|
290
|
+
file: target.file,
|
|
291
|
+
status: 'absent',
|
|
292
|
+
detail: `no baseline file; run: ${CLI} sync --yes`,
|
|
293
|
+
remedy: `${CLI} sync --yes`,
|
|
294
|
+
},
|
|
295
|
+
];
|
|
296
|
+
}
|
|
297
|
+
const state = parse(target.previous, target.style);
|
|
298
|
+
if (state.status === 'absent') {
|
|
299
|
+
return [
|
|
300
|
+
{
|
|
301
|
+
file: target.file,
|
|
302
|
+
status: 'unmanaged',
|
|
303
|
+
detail: `the file exists with no simt-dev region; run: ${CLI} sync --yes`,
|
|
304
|
+
remedy: `${CLI} sync --yes`,
|
|
305
|
+
},
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
const off = misplaced(state, target.style);
|
|
309
|
+
return [
|
|
310
|
+
{ file: target.file, ...regionVerdict(target.action), recordedSha: state.sha ?? null },
|
|
311
|
+
...(off ? [{ file: target.file, status: 'misplaced', ...off }] : []),
|
|
312
|
+
];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const surface = target.surface ? { surface: target.surface } : {};
|
|
316
|
+
|
|
317
|
+
if (!target.exists) {
|
|
318
|
+
if (target.readError !== null && target.readError !== undefined && target.readError !== 'ENOENT') {
|
|
319
|
+
return [{ file: target.file, status: 'unreadable', detail: target.readError }];
|
|
320
|
+
}
|
|
321
|
+
return [
|
|
322
|
+
target.surface
|
|
323
|
+
? {
|
|
324
|
+
file: target.file,
|
|
325
|
+
...surface,
|
|
326
|
+
status: 'absent',
|
|
327
|
+
detail: `no ${target.surface} rule here; run: ${CLI} sync --yes`,
|
|
328
|
+
}
|
|
329
|
+
: { file: target.file, status: 'absent', detail: `no AGENTS.md; run: ${CLI} sync --yes` },
|
|
330
|
+
];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const state = parse(target.previous, target.style);
|
|
334
|
+
if (state.status === 'malformed') {
|
|
335
|
+
return [{ file: target.file, ...surface, status: 'malformed', detail: state.reason, line: state.line }];
|
|
336
|
+
}
|
|
337
|
+
if (state.status === 'absent') {
|
|
338
|
+
return [
|
|
339
|
+
{
|
|
340
|
+
file: target.file,
|
|
341
|
+
...surface,
|
|
342
|
+
status: 'unmanaged',
|
|
343
|
+
detail: `no generated region; run: ${CLI} sync --yes`,
|
|
344
|
+
},
|
|
345
|
+
];
|
|
346
|
+
}
|
|
347
|
+
const finding = { file: target.file, ...surface, ...regionVerdict(target.action), recordedSha: state.sha ?? null };
|
|
348
|
+
// Only a region can say what it expected to find: a rule file's marker carries a
|
|
349
|
+
// source list instead, and inventing the field here would describe a shape it never has.
|
|
350
|
+
if (target.kind === 'region') finding.expectedSha = target.sha;
|
|
351
|
+
return [finding];
|
|
352
|
+
}
|
|
353
|
+
|
|
271
354
|
/** Check one repo without writing. Returns findings rather than an exit code,
|
|
272
355
|
* so commands/check decides severity and commands/sync decides whether to act. */
|
|
273
356
|
export async function checkTargets(root, flags = {}) {
|
|
357
|
+
const projection = await plan(root, flags);
|
|
274
358
|
const findings = [];
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
text = await readFile(join(root, PRIMARY_TARGET), 'utf8');
|
|
281
|
-
} catch {
|
|
282
|
-
text = '';
|
|
283
|
-
}
|
|
284
|
-
findings.push(...readLimitFindings(text, PRIMARY_TARGET));
|
|
285
|
-
}
|
|
359
|
+
// The plan's own targets, in the order a reader meets them: the root region, then the
|
|
360
|
+
// packages. Nothing here rebuilds a file.
|
|
361
|
+
const primary = projection.targets.find((t) => t.kind === 'region' && t.file === PRIMARY_TARGET);
|
|
362
|
+
findings.push(...targetFindings(primary));
|
|
363
|
+
findings.push(...readLimitFindings(primary.previous ?? '', PRIMARY_TARGET));
|
|
286
364
|
|
|
287
365
|
// Every package of a monorepo is a projection root in its own right, so the gate
|
|
288
366
|
// covers it the way it covers the root. A nested region that drifts is as stale as
|
|
289
|
-
// the root
|
|
367
|
+
// the root region, and the reader working in that package reads only that file.
|
|
290
368
|
//
|
|
291
369
|
// Gated on the root having a region at all: a monorepo that has not adopted has no
|
|
292
370
|
// nested regions either, and reporting one as absent would describe a half-adoption
|
|
293
371
|
// that nobody performed.
|
|
294
|
-
const
|
|
295
|
-
for (const
|
|
296
|
-
const
|
|
297
|
-
|
|
372
|
+
const profilesOf = new Map(projection.packages.map((p) => [p.path + '/' + PRIMARY_TARGET, p.profiles]));
|
|
373
|
+
for (const target of projection.targets.filter((t) => t.kind === 'region' && t.file !== PRIMARY_TARGET)) {
|
|
374
|
+
for (const finding of targetFindings(target)) {
|
|
375
|
+
findings.push({ ...finding, profiles: profilesOf.get(target.file) });
|
|
376
|
+
}
|
|
298
377
|
}
|
|
299
378
|
|
|
300
379
|
// The gate has to run where the merge happens, not only on the machine of whoever
|
|
@@ -410,35 +489,9 @@ export async function checkTargets(root, flags = {}) {
|
|
|
410
489
|
const sha = await provenanceDigest();
|
|
411
490
|
const notes = [];
|
|
412
491
|
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
// exact asymmetry the region-versus-file gate exists to prevent.
|
|
417
|
-
const scopedRules = desired.scoped ?? [];
|
|
418
|
-
if (desired.narrowing === true) {
|
|
419
|
-
for (const target of (await scopedTargets(root, scopedRules, sha, resolved, notes)).targets) {
|
|
420
|
-
if (!target.exists) {
|
|
421
|
-
findings.push({
|
|
422
|
-
file: target.file,
|
|
423
|
-
surface: target.surface,
|
|
424
|
-
status: 'absent',
|
|
425
|
-
detail: `no ${target.surface} rule here; run: ${CLI} sync --yes`,
|
|
426
|
-
});
|
|
427
|
-
continue;
|
|
428
|
-
}
|
|
429
|
-
// The writer's own verdict, from the same inject call `plan` will make. A second
|
|
430
|
-
// construction here would be a third place computing the same file, which is the
|
|
431
|
-
// defect this whole function exists to avoid.
|
|
432
|
-
const action = target.action;
|
|
433
|
-
findings.push({
|
|
434
|
-
file: target.file,
|
|
435
|
-
surface: target.surface,
|
|
436
|
-
...regionVerdict(action),
|
|
437
|
-
recordedSha: parse(target.previous).sha ?? null,
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
492
|
+
// A rule with a defect is a gate failure even when its files happen to be correct,
|
|
493
|
+
// because the next `sync` will not reproduce it. The defect is a property of the
|
|
494
|
+
// rule; the verdict on a file is below, and it is the writer's own.
|
|
442
495
|
for (const rule of rules) {
|
|
443
496
|
if (rule.error !== null) {
|
|
444
497
|
findings.push({
|
|
@@ -446,106 +499,20 @@ export async function checkTargets(root, flags = {}) {
|
|
|
446
499
|
status: 'invalid',
|
|
447
500
|
detail: rule.error,
|
|
448
501
|
});
|
|
449
|
-
continue;
|
|
450
|
-
}
|
|
451
|
-
// The writer's own construction, not a second one: a body rendered without the
|
|
452
|
-
// surface-agreed substitutions would make `check` report drift on a tree that
|
|
453
|
-
// `init` just wrote (S45's askTool).
|
|
454
|
-
const { body, problems } = renderMdc(
|
|
455
|
-
rule,
|
|
456
|
-
ruleValues(rule, declared.values[rule.id] ?? {}, resolved.surfaces),
|
|
457
|
-
signals,
|
|
458
|
-
);
|
|
459
|
-
if (problems.length > 0) {
|
|
460
|
-
findings.push({
|
|
461
|
-
file: rule.target,
|
|
462
|
-
status: 'invalid',
|
|
463
|
-
detail: problems[0],
|
|
464
|
-
});
|
|
465
|
-
continue;
|
|
466
502
|
}
|
|
503
|
+
}
|
|
467
504
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
file: rel,
|
|
473
|
-
surface: target.surface,
|
|
474
|
-
status: 'absent',
|
|
475
|
-
detail: `no ${target.surface} rule here; run: ${CLI} sync --yes`,
|
|
476
|
-
});
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
const state = parse(target.previous);
|
|
480
|
-
if (state.status === 'malformed') {
|
|
481
|
-
findings.push({
|
|
482
|
-
file: rel,
|
|
483
|
-
surface: target.surface,
|
|
484
|
-
status: 'malformed',
|
|
485
|
-
detail: state.reason,
|
|
486
|
-
line: state.line,
|
|
487
|
-
});
|
|
488
|
-
continue;
|
|
489
|
-
}
|
|
490
|
-
if (state.status === 'absent') {
|
|
491
|
-
findings.push({
|
|
492
|
-
file: rel,
|
|
493
|
-
surface: target.surface,
|
|
494
|
-
status: 'unmanaged',
|
|
495
|
-
detail: `no generated region; run: ${CLI} sync --yes`,
|
|
496
|
-
});
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
// `target.action` is `ruleTargets`' own verdict, produced by the same inject
|
|
500
|
-
// call `plan` will make. This used to reconstruct a reference here — existing
|
|
501
|
-
// frontmatter plus a fresh inject — which was a THIRD construction of the same
|
|
502
|
-
// file inside the one function whose entire job is to say whether the other two
|
|
503
|
-
// agree. It happened to agree, which is what makes this duplication dangerous:
|
|
504
|
-
// it is only detectable by the disagreement some future edit produces.
|
|
505
|
-
const action = target.action;
|
|
506
|
-
findings.push({
|
|
507
|
-
file: rel,
|
|
508
|
-
surface: target.surface,
|
|
509
|
-
...regionVerdict(action),
|
|
510
|
-
recordedSha: state.sha ?? null,
|
|
511
|
-
});
|
|
512
|
-
}
|
|
505
|
+
// Every rule file the plan would write, in the plan's order: the surfaces' files,
|
|
506
|
+
// then the profiles that narrowed out of the region.
|
|
507
|
+
for (const target of projection.targets.filter((t) => t.surface)) {
|
|
508
|
+
findings.push(...targetFindings(target));
|
|
513
509
|
}
|
|
514
510
|
|
|
515
511
|
// Baselines are checked against the same construction `sync` writes from, and they
|
|
516
512
|
// block for the same reason the rule files block: an adopting repo whose baseline was
|
|
517
513
|
// deleted or reverted is a repo whose editors have quietly stopped agreeing.
|
|
518
|
-
for (const
|
|
519
|
-
|
|
520
|
-
findings.push({ file: t.file, status: 'malformed', detail: t.reason, line: t.line });
|
|
521
|
-
continue;
|
|
522
|
-
}
|
|
523
|
-
if (!t.exists) {
|
|
524
|
-
findings.push({
|
|
525
|
-
file: t.file,
|
|
526
|
-
status: 'absent',
|
|
527
|
-
detail: `no baseline file; run: ${CLI} sync --yes`,
|
|
528
|
-
remedy: `${CLI} sync --yes`,
|
|
529
|
-
});
|
|
530
|
-
continue;
|
|
531
|
-
}
|
|
532
|
-
const state = parse(t.previous, t.style);
|
|
533
|
-
if (state.status === 'absent') {
|
|
534
|
-
findings.push({
|
|
535
|
-
file: t.file,
|
|
536
|
-
status: 'unmanaged',
|
|
537
|
-
detail: `the file exists with no simt-dev region; run: ${CLI} sync --yes`,
|
|
538
|
-
remedy: `${CLI} sync --yes`,
|
|
539
|
-
});
|
|
540
|
-
continue;
|
|
541
|
-
}
|
|
542
|
-
findings.push({
|
|
543
|
-
file: t.file,
|
|
544
|
-
...regionVerdict(t.action),
|
|
545
|
-
recordedSha: state.sha ?? null,
|
|
546
|
-
});
|
|
547
|
-
const off = misplaced(state, t.style);
|
|
548
|
-
if (off) findings.push({ file: t.file, status: 'misplaced', ...off });
|
|
514
|
+
for (const target of projection.targets.filter((t) => t.kind === 'baseline')) {
|
|
515
|
+
findings.push(...targetFindings(target));
|
|
549
516
|
}
|
|
550
517
|
|
|
551
518
|
// Managed rule files this run would NOT write, found by scanning the rule
|
|
@@ -665,72 +632,10 @@ async function orphanedRuleFiles(root, resolved) {
|
|
|
665
632
|
}
|
|
666
633
|
|
|
667
634
|
export async function checkTarget(root, flags = {}) {
|
|
668
|
-
const
|
|
669
|
-
return
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
async function checkPrimary(root, flags = {}) {
|
|
673
|
-
const { body, meta } = await desiredRegion(root, flags);
|
|
674
|
-
return checkRegion(root, root, body, meta);
|
|
635
|
+
const projection = await plan(root, flags);
|
|
636
|
+
return targetFindings(projection.targets.find((t) => t.kind === 'region' && t.file === PRIMARY_TARGET))[0];
|
|
675
637
|
}
|
|
676
638
|
|
|
677
|
-
/**
|
|
678
|
-
* One region, compared against what this simt-dev would write for it.
|
|
679
|
-
*
|
|
680
|
-
* `dir` is the projection root that owns the document: the repository for the root
|
|
681
|
-
* region, a package for a nested one. `regionOpts` is read from the repository
|
|
682
|
-
* either way, because placement is a repository-level decision.
|
|
683
|
-
*/
|
|
684
|
-
async function checkRegion(root, dir, body, meta) {
|
|
685
|
-
const path = join(dir, PRIMARY_TARGET);
|
|
686
|
-
let existing = '';
|
|
687
|
-
try {
|
|
688
|
-
existing = await readFile(path, 'utf8');
|
|
689
|
-
} catch (e) {
|
|
690
|
-
// Only ENOENT is absence. A bare `catch` here once reported "no AGENTS.md" for a
|
|
691
|
-
// repo that has one, because the failure was a ReferenceError from a missing
|
|
692
|
-
// import — the gate answered confidently about a file it had never read. A check
|
|
693
|
-
// that is wrong about existence is worse than one that crashes.
|
|
694
|
-
if (e.code !== 'ENOENT') {
|
|
695
|
-
return { file: PRIMARY_TARGET, status: 'unreadable', detail: e.code ?? String(e) };
|
|
696
|
-
}
|
|
697
|
-
return { file: PRIMARY_TARGET, status: 'absent', detail: `no AGENTS.md; run: ${CLI} sync --yes` };
|
|
698
|
-
}
|
|
699
|
-
const state = parse(existing);
|
|
700
|
-
if (state.status === 'malformed') {
|
|
701
|
-
return {
|
|
702
|
-
file: PRIMARY_TARGET,
|
|
703
|
-
status: 'malformed',
|
|
704
|
-
detail: state.reason,
|
|
705
|
-
line: state.line,
|
|
706
|
-
};
|
|
707
|
-
}
|
|
708
|
-
if (state.status === 'absent') {
|
|
709
|
-
return { file: PRIMARY_TARGET, status: 'unmanaged', detail: `no generated region; run: ${CLI} sync --yes` };
|
|
710
|
-
}
|
|
711
|
-
const d = diff(existing, body, meta, await regionOpts(root));
|
|
712
|
-
return {
|
|
713
|
-
file: PRIMARY_TARGET,
|
|
714
|
-
...regionVerdict(d.action),
|
|
715
|
-
recordedSha: state.sha ?? null,
|
|
716
|
-
expectedSha: meta.sha,
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
/**
|
|
720
|
-
* The bytes of always-on projection this repository carries, or null if it has not
|
|
721
|
-
* adopted at all.
|
|
722
|
-
*
|
|
723
|
-
* Delegates to the meter rather than counting here, and that is the whole of the
|
|
724
|
-
* design. A first version summed the region plus the rule files this function already
|
|
725
|
-
* compares, which sounds cheaper and is wrong: the meter also counts the managed skills
|
|
726
|
-
* and the `.mdc` files a reader loads, so the gate and the report came to different
|
|
727
|
-
* totals for one repository. Two definitions of one budget is the defect ADR-034 was
|
|
728
|
-
* filed about, rebuilt one layer up. The meter is the definition; the gate quotes it.
|
|
729
|
-
*
|
|
730
|
-
* The cost is that `check` now reads the documents the meter reads, which it did not
|
|
731
|
-
* do before. That is a real cost, paid knowingly: the alternative is a gate that fails
|
|
732
|
-
* on a number the report does not print, which is worse than a slower gate.
|
|
733
|
-
*/
|
|
734
639
|
async function alwaysOnBytes(root) {
|
|
735
640
|
const { exists } = await readManifest(root);
|
|
736
641
|
if (!exists) return null;
|