simt-dev 0.2.0 → 0.2.2
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 +375 -7
- package/README.md +4 -4
- package/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +27 -0
- package/docs/decisions/ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md +75 -0
- package/docs/decisions/ADR-060-a-repository-with-nothing-to-run-is-adopted-and-says-so.md +131 -0
- package/docs/decisions/ADR-061-the-skill-registry-is-workspace-data.md +113 -0
- package/docs/decisions/ADR-062-a-fresh-adoption-seeds-the-gates-it-can-run.md +94 -0
- package/docs/decisions/ADR-063-the-adoption-escape-hatch-is-opt-in.md +69 -0
- package/docs/decisions/ADR-064-the-context-meter-counts-what-the-tool-wrote.md +107 -0
- package/docs/decisions/ADR-065-a-pack-is-one-shared-rule-set-pinned-by-digest.md +97 -0
- package/docs/decisions/ADR-066-a-monorepo-projects-once-per-package.md +115 -0
- package/docs/decisions/ADR-067-which-harnesses-read-agents-md-is-a-column.md +110 -0
- package/docs/decisions/ADR-068-the-mcp-revision-list-cites-what-was-read.md +111 -0
- package/docs/decisions/ADR-069-the-conformance-lint-checks-the-published-specification.md +90 -0
- package/docs/decisions/ADR-070-the-json-payload-carries-its-own-version.md +91 -0
- package/docs/decisions/ADR-071-a-rule-that-can-be-narrowed-is-written-narrow.md +106 -0
- package/docs/decisions/ADR-072-the-budget-is-a-declaration-beside-the-gates.md +104 -0
- package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +92 -0
- package/docs/decisions/README.md +17 -0
- package/package.json +1 -2
- package/rules/core.md +1 -0
- package/rules/stacks/android.md +2 -0
- package/rules/stacks/cloudflare-workers.md +2 -0
- package/rules/stacks/connectiq.md +1 -0
- package/rules/stacks/container.md +2 -0
- package/rules/stacks/dart.md +40 -0
- package/rules/stacks/deno.md +38 -0
- package/rules/stacks/dotnet.md +37 -0
- package/rules/stacks/elixir.md +41 -0
- package/rules/stacks/go.md +36 -0
- package/rules/stacks/jvm.md +2 -0
- package/rules/stacks/node.md +2 -0
- package/rules/stacks/python.md +2 -0
- package/rules/stacks/react-native.md +2 -0
- package/rules/stacks/react-web.md +2 -0
- package/rules/stacks/ruby.md +38 -0
- package/rules/stacks/rust.md +36 -0
- package/rules/stacks/swift.md +39 -0
- package/rules/stacks/typescript.md +2 -0
- package/rules/stacks/uv.md +2 -0
- package/src/argv.mjs +54 -6
- package/src/ci/dependabot.mjs +1 -1
- package/src/ci/workflow.mjs +33 -11
- package/src/commands/compare.mjs +15 -0
- package/src/commands/graph.mjs +58 -25
- package/src/commands/harness.mjs +150 -0
- package/src/commands/help.mjs +37 -5
- package/src/commands/hooks.mjs +173 -57
- package/src/commands/index.mjs +95 -69
- package/src/commands/init.mjs +100 -20
- package/src/commands/mcp.mjs +20 -5
- package/src/commands/rules.mjs +96 -60
- package/src/commands/skills.mjs +189 -86
- package/src/commands/skillsinstall.mjs +20 -5
- package/src/commands/sync.mjs +213 -15
- package/src/commands/tokens.mjs +138 -0
- package/src/compare/hooksstate.mjs +96 -9
- package/src/compare/row.mjs +107 -7
- package/src/decisions/index.mjs +21 -6
- package/src/graph/load.mjs +125 -14
- package/src/graph/spec.mjs +227 -0
- package/src/graph/speccheck.mjs +68 -0
- package/src/hooks/capability.mjs +48 -4
- package/src/hooks/dispatcher.mjs +84 -7
- package/src/hooks/gates.mjs +0 -2
- package/src/hooks/resolve.mjs +24 -25
- package/src/hooks/shim.mjs +29 -5
- package/src/identity.mjs +0 -11
- package/src/mcp/schema.mjs +139 -0
- package/src/mcp/server.mjs +197 -20
- package/src/mcp/tools.mjs +290 -3
- package/src/measure/context.mjs +371 -0
- package/src/monorepo.mjs +339 -0
- package/src/output/index.mjs +86 -9
- package/src/package-version.mjs +74 -0
- package/src/packs.mjs +182 -0
- package/src/projection/conflicts.mjs +56 -5
- package/src/projection/manifest.mjs +335 -25
- package/src/projection/mdc.mjs +4 -13
- package/src/projection/project.mjs +139 -8
- package/src/projection/rules.mjs +308 -27
- package/src/projection/surfaces.mjs +133 -15
- package/src/projection/targets.mjs +90 -7
- package/src/resolve/eval.mjs +64 -0
- package/src/resolve/fingerprint.mjs +1 -0
- package/src/resolve/near.mjs +26 -7
- package/src/resolve/resolve.mjs +1 -1
- package/src/resolve/source.mjs +1 -1
- package/src/workspace.mjs +0 -9
- package/graph/skills.graph.json +0 -1622
|
@@ -25,10 +25,11 @@ import { planDependabot } from '../ci/dependabot.mjs';
|
|
|
25
25
|
import { checkTsconfig } from '../tsconfig.mjs';
|
|
26
26
|
import { checkToolchain } from '../toolchain.mjs';
|
|
27
27
|
import { diff, parse, inject, BEGIN, END } from './markers.mjs';
|
|
28
|
-
import { readManifest } from './manifest.mjs';
|
|
28
|
+
import { budgetFinding, readManifest } from './manifest.mjs';
|
|
29
|
+
import { measureRoot } from '../measure/context.mjs';
|
|
29
30
|
import { loadMdcRules, renderMdc } from './mdc.mjs';
|
|
30
31
|
import { foldsRules, resolveSurfaces, SURFACES, SURFACE_IDS } from './surfaces.mjs';
|
|
31
|
-
import { pointerState, pointerTargets, ruleTargets } from './targets.mjs';
|
|
32
|
+
import { pointerState, pointerTargets, ruleTargets, scopedTargets } from './targets.mjs';
|
|
32
33
|
import { CLI } from '../name.mjs';
|
|
33
34
|
import { MANIFEST_FILE } from '../name.mjs';
|
|
34
35
|
import {
|
|
@@ -36,6 +37,7 @@ import {
|
|
|
36
37
|
provenanceDigest,
|
|
37
38
|
isEligible,
|
|
38
39
|
loadRules,
|
|
40
|
+
packageRegions,
|
|
39
41
|
LOCAL_PROFILE_DIR,
|
|
40
42
|
mdcTargets,
|
|
41
43
|
readDeclared,
|
|
@@ -69,12 +71,31 @@ export async function plan(root, flags = {}) {
|
|
|
69
71
|
|
|
70
72
|
const surfaces = resolveSurfaces(manifest, flags);
|
|
71
73
|
|
|
74
|
+
// Profiles that narrowed out of the region, written as files for the surfaces whose
|
|
75
|
+
// scoping key was read in the vendor's own documentation. Only when EVERY selected
|
|
76
|
+
// surface can scope: a profile that left the region while a reader could not narrow
|
|
77
|
+
// would be a rule that reader never loads, and `desiredRegion` already kept it in the
|
|
78
|
+
// region in that case, which is why `narrowing` is the gate here too.
|
|
79
|
+
const scoped =
|
|
80
|
+
desired.narrowing === true
|
|
81
|
+
? await scopedTargets(root, desired.scoped ?? [], meta.sha, surfaces, notes)
|
|
82
|
+
: { targets: [] };
|
|
83
|
+
|
|
72
84
|
// The baselines are the same list shape as every other target, so `sync` writes
|
|
73
85
|
// them and reports them in the same plan it prints for AGENTS.md and the rule
|
|
74
86
|
// files. A file the reader was not shown is a file they approve by reading the diff
|
|
75
87
|
// afterwards, which is the one review step this tool exists to make unnecessary.
|
|
76
88
|
const baselines = await baselineTargets(root, meta.sha);
|
|
77
89
|
|
|
90
|
+
// A monorepo projects once per package as well as once at the root. The nested
|
|
91
|
+
// regions come after the root's and before the rule files, because the root is
|
|
92
|
+
// what a reader meets first and a plan that listed a package's region before the
|
|
93
|
+
// root's would read as though the packages were the repository.
|
|
94
|
+
const packages = await packageRegions(root, flags, profiles);
|
|
95
|
+
const nested = await Promise.all(
|
|
96
|
+
packages.regions.map((r) => targetFor(join(root, r.path), r.body, r.meta, `${r.path}/${PRIMARY_TARGET}`)),
|
|
97
|
+
);
|
|
98
|
+
|
|
78
99
|
return {
|
|
79
100
|
root,
|
|
80
101
|
stack,
|
|
@@ -85,13 +106,14 @@ export async function plan(root, flags = {}) {
|
|
|
85
106
|
surfaceSource: surfaces.source,
|
|
86
107
|
surfaceProblems: surfaces.problems,
|
|
87
108
|
rules: all,
|
|
88
|
-
targets: [target, ...mdc.targets, ...baselines.targets],
|
|
109
|
+
targets: [target, ...nested, ...mdc.targets, ...scoped.targets, ...baselines.targets],
|
|
110
|
+
packages: packages.regions.map((r) => ({ path: r.path, profiles: r.profiles })),
|
|
89
111
|
mdcProblems: mdc.problems,
|
|
90
|
-
notes: [...notes, ...mdc.notes],
|
|
112
|
+
notes: [...notes, ...mdc.notes, ...packages.notes],
|
|
91
113
|
};
|
|
92
114
|
}
|
|
93
115
|
|
|
94
|
-
async function targetFor(root, body, meta) {
|
|
116
|
+
async function targetFor(root, body, meta, label = PRIMARY_TARGET) {
|
|
95
117
|
const path = join(root, PRIMARY_TARGET);
|
|
96
118
|
let existing = '';
|
|
97
119
|
let exists = true;
|
|
@@ -103,7 +125,10 @@ async function targetFor(root, body, meta) {
|
|
|
103
125
|
|
|
104
126
|
const result = inject(existing, body, meta, await regionOpts(root));
|
|
105
127
|
return {
|
|
106
|
-
|
|
128
|
+
// The repo-relative path, and not the basename: a monorepo writes the same
|
|
129
|
+
// document once per package, and a plan that printed `AGENTS.md` twice with no
|
|
130
|
+
// directory on either line would leave the reader unable to tell which is which.
|
|
131
|
+
file: label,
|
|
107
132
|
path,
|
|
108
133
|
exists,
|
|
109
134
|
action: result.action,
|
|
@@ -140,6 +165,36 @@ export async function checkTargets(root, flags = {}) {
|
|
|
140
165
|
findings.push(...readLimitFindings(text, PRIMARY_TARGET));
|
|
141
166
|
}
|
|
142
167
|
|
|
168
|
+
// The budget: what this repository agreed to carry, against what it carries.
|
|
169
|
+
//
|
|
170
|
+
// Measured from the same `desiredRegion` the region above was compared against, plus
|
|
171
|
+
// the rule files this function went on to compare, because a gate and a meter that
|
|
172
|
+
// each counted their own set would report two different totals for one repository and
|
|
173
|
+
// the reader would have no way to tell which was the budget. Both numbers are the
|
|
174
|
+
// DESIRED ones, so a drifted repo is reported against what it would carry after a
|
|
175
|
+
// sync rather than what it carries today.
|
|
176
|
+
//
|
|
177
|
+
// Non-blocking by design: nothing is cut, because there is nothing to cut yet (ADR-072).
|
|
178
|
+
const budgetTotal = await alwaysOnBytes(root);
|
|
179
|
+
if (budgetTotal !== null) {
|
|
180
|
+
const { manifest } = await readManifest(root);
|
|
181
|
+
const over = budgetFinding(budgetTotal.bytes, manifest?.budget, manifest?.mode);
|
|
182
|
+
if (over) findings.push({ ...over, file: MANIFEST_FILE, detail: over.detail });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Every package of a monorepo is a projection root in its own right, so the gate
|
|
186
|
+
// covers it the way it covers the root. A nested region that drifts is as stale as
|
|
187
|
+
// the root's region, and the reader working in that package reads only that file.
|
|
188
|
+
//
|
|
189
|
+
// Gated on the root having a region at all: a monorepo that has not adopted has no
|
|
190
|
+
// nested regions either, and reporting one as absent would describe a half-adoption
|
|
191
|
+
// that nobody performed.
|
|
192
|
+
const { regions: packageBodies } = await packageRegions(root, flags);
|
|
193
|
+
for (const region of packageBodies) {
|
|
194
|
+
const file = `${region.path}/${PRIMARY_TARGET}`;
|
|
195
|
+
findings.push({ ...(await checkRegion(root, join(root, region.path), region.body, region.meta)), file, profiles: region.profiles });
|
|
196
|
+
}
|
|
197
|
+
|
|
143
198
|
// The gate has to run where the merge happens, not only on the machine of whoever
|
|
144
199
|
// remembers to run it. A repo that already runs GitHub Actions and has no simt-dev
|
|
145
200
|
// job is ungated at its front door, and that is the one finding here that no amount
|
|
@@ -189,7 +244,8 @@ export async function checkTargets(root, flags = {}) {
|
|
|
189
244
|
// Selection must match `plan` exactly, or the gate reports a rule absent that
|
|
190
245
|
// `sync` was never going to write — a permanently red check nothing can fix.
|
|
191
246
|
const all = await loadMdcRules();
|
|
192
|
-
const
|
|
247
|
+
const desired = await desiredRegion(root, flags);
|
|
248
|
+
const { stack, facts, manifest } = desired;
|
|
193
249
|
const declared = await readDeclared(root);
|
|
194
250
|
const resolved = resolveSurfaces(manifest, flags);
|
|
195
251
|
for (const p of resolved.problems) {
|
|
@@ -200,6 +256,36 @@ export async function checkTargets(root, flags = {}) {
|
|
|
200
256
|
const sha = await provenanceDigest();
|
|
201
257
|
const notes = [];
|
|
202
258
|
|
|
259
|
+
// The profiles that left the region, gated through the same construction that wrote
|
|
260
|
+
// them. Without this loop a scoped file is planned and written but never checked, so a
|
|
261
|
+
// repository that edits or deletes one shows a clean `check` and a stale reader — the
|
|
262
|
+
// exact asymmetry the region-versus-file gate exists to prevent.
|
|
263
|
+
const scopedRules = desired.scoped ?? [];
|
|
264
|
+
if (desired.narrowing === true) {
|
|
265
|
+
for (const target of (await scopedTargets(root, scopedRules, sha, resolved, notes)).targets) {
|
|
266
|
+
if (!target.exists) {
|
|
267
|
+
findings.push({
|
|
268
|
+
file: target.file,
|
|
269
|
+
surface: target.surface,
|
|
270
|
+
status: 'absent',
|
|
271
|
+
detail: `no ${target.surface} rule here; run: ${CLI} sync --yes`,
|
|
272
|
+
});
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
// The writer's own verdict, from the same inject call `plan` will make. A second
|
|
276
|
+
// construction here would be a third place computing the same file, which is the
|
|
277
|
+
// defect this whole function exists to avoid.
|
|
278
|
+
const action = target.action;
|
|
279
|
+
findings.push({
|
|
280
|
+
file: target.file,
|
|
281
|
+
surface: target.surface,
|
|
282
|
+
status: action === 'unchanged' ? 'current' : 'drift',
|
|
283
|
+
detail: action === 'unchanged' ? undefined : `region is ${action}; run: ${CLI} sync --yes`,
|
|
284
|
+
recordedSha: parse(target.previous).sha ?? null,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
203
289
|
for (const rule of rules) {
|
|
204
290
|
if (rule.error !== null) {
|
|
205
291
|
findings.push({
|
|
@@ -433,7 +519,18 @@ export async function checkTarget(root, flags = {}) {
|
|
|
433
519
|
|
|
434
520
|
async function checkPrimary(root, flags = {}) {
|
|
435
521
|
const { body, meta } = await desiredRegion(root, flags);
|
|
436
|
-
|
|
522
|
+
return checkRegion(root, root, body, meta);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* One region, compared against what this simt-dev would write for it.
|
|
527
|
+
*
|
|
528
|
+
* `dir` is the projection root that owns the document: the repository for the root
|
|
529
|
+
* region, a package for a nested one. `regionOpts` is read from the repository
|
|
530
|
+
* either way, because placement is a repository-level decision.
|
|
531
|
+
*/
|
|
532
|
+
async function checkRegion(root, dir, body, meta) {
|
|
533
|
+
const path = join(dir, PRIMARY_TARGET);
|
|
437
534
|
let existing = '';
|
|
438
535
|
try {
|
|
439
536
|
existing = await readFile(path, 'utf8');
|
|
@@ -468,6 +565,28 @@ async function checkPrimary(root, flags = {}) {
|
|
|
468
565
|
expectedSha: meta.sha,
|
|
469
566
|
};
|
|
470
567
|
}
|
|
568
|
+
/**
|
|
569
|
+
* The bytes of always-on projection this repository carries, or null if it has not
|
|
570
|
+
* adopted at all.
|
|
571
|
+
*
|
|
572
|
+
* Delegates to the meter rather than counting here, and that is the whole of the
|
|
573
|
+
* design. A first version summed the region plus the rule files this function already
|
|
574
|
+
* compares, which sounds cheaper and is wrong: the meter also counts the managed skills
|
|
575
|
+
* and the `.mdc` files a reader loads, so the gate and the report came to different
|
|
576
|
+
* totals for one repository. Two definitions of one budget is the defect ADR-034 was
|
|
577
|
+
* filed about, rebuilt one layer up. The meter is the definition; the gate quotes it.
|
|
578
|
+
*
|
|
579
|
+
* The cost is that `check` now reads the documents the meter reads, which it did not
|
|
580
|
+
* do before. That is a real cost, paid knowingly: the alternative is a gate that fails
|
|
581
|
+
* on a number the report does not print, which is worse than a slower gate.
|
|
582
|
+
*/
|
|
583
|
+
async function alwaysOnBytes(root) {
|
|
584
|
+
const { exists } = await readManifest(root);
|
|
585
|
+
if (!exists) return null;
|
|
586
|
+
const measured = await measureRoot(root);
|
|
587
|
+
return { bytes: measured.generatedBytes };
|
|
588
|
+
}
|
|
589
|
+
|
|
471
590
|
/** `projection.place: "top"` puts the AGENTS.md region above the hand-written prefix. */
|
|
472
591
|
async function regionOpts(root) {
|
|
473
592
|
const { manifest } = await readManifest(root);
|
|
@@ -478,6 +597,10 @@ async function regionOpts(root) {
|
|
|
478
597
|
* A reader that stops at a byte offset never sees a region that ends past it.
|
|
479
598
|
* The finding names the reader, because "the file is long" does not say who stops.
|
|
480
599
|
*/
|
|
600
|
+
// Exported for the context meter (`simt-dev tokens`), which answers the same question
|
|
601
|
+
// about a region rather than about a diff. It reads the structured fields instead of
|
|
602
|
+
// re-deriving the limit, because a second implementation of "which reader stops, and
|
|
603
|
+
// where" is a second thing to keep right - and a second thing to quote in a message.
|
|
481
604
|
export function readLimitFindings(text, file) {
|
|
482
605
|
const state = parse(text);
|
|
483
606
|
if (state.status !== 'ok' || typeof state.endByte !== 'number') return [];
|
|
@@ -489,6 +612,14 @@ export function readLimitFindings(text, file) {
|
|
|
489
612
|
findings.push({
|
|
490
613
|
file,
|
|
491
614
|
status: 'beyond-read-limit',
|
|
615
|
+
// The numbers, not only the sentence. `check` prints the detail and the context
|
|
616
|
+
// meter puts the reader, the limit and the offset in its JSON, and a consumer
|
|
617
|
+
// that has to regex prose to get them is a consumer that breaks when the prose
|
|
618
|
+
// is edited.
|
|
619
|
+
reader: limit.reader,
|
|
620
|
+
limitBytes: limit.bytes,
|
|
621
|
+
endByte: state.endByte,
|
|
622
|
+
overBy: state.endByte - limit.bytes,
|
|
492
623
|
detail: `BEYOND-READ-LIMIT: the region ends at byte ${state.endByte}, past ${limit.reader}'s ${limit.bytes}-byte read of ${file}`,
|
|
493
624
|
remedy: `set projection.place to "top" in ${MANIFEST_FILE} and run: ${CLI} sync --yes, or raise ${limit.reader}'s project_doc_max_bytes`,
|
|
494
625
|
});
|