simt-dev 0.1.1 → 0.2.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 +36 -0
- package/README.md +31 -4
- package/docs/decisions/ADR-001-push-requires-approval.md +5 -5
- package/docs/decisions/ADR-002-refactor-blast-radius.md +1 -1
- package/docs/decisions/ADR-003-test-breadth.md +4 -4
- package/docs/decisions/ADR-004-hook-owns-format.md +5 -5
- package/docs/decisions/ADR-005-file-length.md +5 -5
- package/docs/decisions/ADR-006-coverage-bar.md +3 -3
- package/docs/decisions/ADR-007-section-skeleton.md +5 -5
- package/docs/decisions/ADR-008-verdict-vocabulary.md +1 -1
- package/docs/decisions/ADR-009-skill-layout.md +1 -1
- package/docs/decisions/ADR-010-runner-invocation.md +1 -1
- package/docs/decisions/ADR-012-drop-windows-support.md +2 -2
- package/docs/decisions/ADR-015-a-source-is-content-not-a-location.md +2 -2
- package/docs/decisions/ADR-016-a-divergent-rule-is-retired-not-merged.md +13 -13
- package/docs/decisions/ADR-017-a-declared-gate-must-be-runnable.md +2 -2
- package/docs/decisions/ADR-018-an-unset-hooks-path-is-not-an-absent-hook.md +9 -9
- package/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +7 -7
- package/docs/decisions/ADR-023-the-gate-ships-as-a-file-the-repo-owns.md +2 -2
- package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +5 -5
- package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +2 -2
- 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 +2 -2
- package/docs/decisions/ADR-031-the-suite-stages-the-environment-it-needs.md +3 -3
- package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +4 -4
- package/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md +2 -2
- package/docs/decisions/ADR-045-a-dashboard-that-counts-a-defect.md +3 -3
- package/docs/decisions/ADR-046-one-rule-one-copy.md +5 -5
- package/docs/decisions/ADR-047-a-profile-selects-on-what-a-repository-runs-on.md +4 -4
- package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +5 -5
- package/docs/decisions/ADR-057-an-identity-is-per-developer-and-a-repository-does-not-own-it.md +115 -0
- package/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +148 -0
- package/docs/decisions/README.md +3 -1
- package/package.json +1 -1
- package/src/commands/hooks.mjs +10 -10
- package/src/commands/init.mjs +67 -1
- package/src/commands/skillsinstall.mjs +1 -1
- package/src/compare/discover.mjs +1 -1
- package/src/compare/row.mjs +2 -2
- package/src/decisions/index.mjs +30 -6
- package/src/graph/load.mjs +1 -1
- package/src/hooks/capability.mjs +1 -1
- package/src/hooks/dispatcher.mjs +1 -1
- package/src/hooks/gates.mjs +1 -1
- package/src/hooks/where.mjs +1 -1
- package/src/identity.mjs +218 -0
- package/src/projection/baselines.mjs +1 -1
- package/src/projection/conflicts.mjs +1 -1
- package/src/projection/manifest.mjs +1 -1
- package/src/projection/markers.mjs +3 -3
- package/src/projection/mdc.mjs +2 -2
- package/src/projection/surfaces.mjs +2 -2
- package/src/resolve/fingerprint.mjs +3 -3
- package/src/resolve/source.mjs +1 -1
- package/src/tsconfig.mjs +1 -1
package/src/compare/row.mjs
CHANGED
|
@@ -78,8 +78,8 @@ export async function buildRow(path, context) {
|
|
|
78
78
|
// Adoption is settled BEFORE any problem is collected. Every gate below asks
|
|
79
79
|
// `row.adoption`, and this block used to run after them - so the stale-rule-file gate
|
|
80
80
|
// below compared against the initialiser's `'unmanaged'` and could never fire. In the
|
|
81
|
-
// estate
|
|
82
|
-
//
|
|
81
|
+
// estate, with 0 of 3 rule files current, report a problems list that mentioned
|
|
82
|
+
// neither the rule files nor anything the exit code could see. A gate
|
|
83
83
|
// that reads a field the line above has not set yet is not a gate.
|
|
84
84
|
//
|
|
85
85
|
// An absent manifest is not a defect; it is the pre-adoption state, which is the normal
|
package/src/decisions/index.mjs
CHANGED
|
@@ -445,8 +445,29 @@ const section = (name) => {
|
|
|
445
445
|
*
|
|
446
446
|
* Not global: callers reuse it with `exec`, and a `/g` flag would remember `lastIndex`.
|
|
447
447
|
*/
|
|
448
|
+
/**
|
|
449
|
+
* Identifiers that belong to the repositories this tool was built against, rather than
|
|
450
|
+
* to anything a reader has.
|
|
451
|
+
*
|
|
452
|
+
* This used to be a list of names, and that was the defect: the guard that exists to
|
|
453
|
+
* keep those names out of shipped text was itself shipped, carrying them. Every
|
|
454
|
+
* adopting repository could read the portfolio out of `src/`.
|
|
455
|
+
*
|
|
456
|
+
* So it matches shapes instead:
|
|
457
|
+
*
|
|
458
|
+
* - `simt-<something>` that is not `simt-dev`, which is most of the estate
|
|
459
|
+
* - `repo-NN`, the pseudonyms the decision records now use in place of a name. These
|
|
460
|
+
* must not leak either: a label that identifies nothing publicly is still a label
|
|
461
|
+
* that joins one record to another, and shipping it into a consumer's repository
|
|
462
|
+
* would carry that join with it.
|
|
463
|
+
*
|
|
464
|
+
* What this no longer catches, stated plainly: three estate repositories whose names
|
|
465
|
+
* do not begin `simt-`, which a shape-based rule cannot recognise without enumerating
|
|
466
|
+
* them - which is what this file used to do. Detection of those now depends on a name
|
|
467
|
+
* never being written into shipped text, rather than on a check.
|
|
468
|
+
*/
|
|
448
469
|
export const ESTATE_NAME =
|
|
449
|
-
/\b(?:simt-(?!dev\b)[\w-]+|
|
|
470
|
+
/\b(?:simt-(?!dev\b)[\w-]+|repo-\d{2})\b/;
|
|
450
471
|
|
|
451
472
|
/** Replace estate repository names with a phrase a reader who does not have them can use. */
|
|
452
473
|
export function redactEstate(text) {
|
|
@@ -477,12 +498,15 @@ export function redactCommand(text) {
|
|
|
477
498
|
}
|
|
478
499
|
|
|
479
500
|
/**
|
|
480
|
-
* Repositories the records were written about
|
|
481
|
-
*
|
|
482
|
-
*
|
|
501
|
+
* Repositories the records were written about, so a maintainer can see which decisions
|
|
502
|
+
* cite which. `rules why` redacts them before printing: the names are not actionable in
|
|
503
|
+
* a repository that does not contain those files.
|
|
504
|
+
*
|
|
505
|
+
* Same shape rule as {@link ESTATE_NAME} and for the same reason - this shipped too, so
|
|
506
|
+
* enumerating the names here put the portfolio in every adopting repository. The records
|
|
507
|
+
* themselves now carry `repo-NN` labels, so that is what this matches in practice.
|
|
483
508
|
*/
|
|
484
|
-
const ESTATE =
|
|
485
|
-
/\b(?:app-ui|bunnyland(?:-server)?|wakt-app|simt-[\w-]+|(?:the )?(?:Social|garmin) repos?)\b/gi;
|
|
509
|
+
const ESTATE = /\b(?:simt-(?!dev\b)[\w-]+|repo-\d{2})\b/gi;
|
|
486
510
|
|
|
487
511
|
/** Trim to whole sentences within a budget, never leaving a stub fragment. */
|
|
488
512
|
function sentence(text, limit = 260) {
|
package/src/graph/load.mjs
CHANGED
|
@@ -478,7 +478,7 @@ async function lintSourcesOnDisk(registry, workspaceRoot) {
|
|
|
478
478
|
/**
|
|
479
479
|
* SKILL.md plus every file beneath the skill directory.
|
|
480
480
|
* Handles both layouts: a directory containing SKILL.md, and the flat
|
|
481
|
-
* `skills/<name>.md` form
|
|
481
|
+
* `skills/<name>.md` form one repository still uses (ADR-009 says that will be
|
|
482
482
|
* wrapped, so lint reports it rather than accepting it silently).
|
|
483
483
|
*/
|
|
484
484
|
export async function measureSkill(full) {
|
package/src/hooks/capability.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// This exists because the first real adoption declared five gates and could run two.
|
|
4
4
|
// The stack table proposes a formatter, a linter and an i18n check for any Node repo;
|
|
5
|
-
//
|
|
5
|
+
// a repository with no prettier installed, no `lint` script and no declared command
|
|
6
6
|
// for the i18n check. Every one of those gates fails closed by design, so installing the
|
|
7
7
|
// dispatcher on that manifest blocks every commit and teaches the team to reach for
|
|
8
8
|
// `--no-verify`. The dispatcher was right; the declaration was wrong.
|
package/src/hooks/dispatcher.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Design commitments, all of them load-bearing:
|
|
4
4
|
//
|
|
5
5
|
// Fail closed, never fail open. A gate that cannot run is a failure, not a
|
|
6
|
-
// skip.
|
|
6
|
+
// skip. A repository removed tests from pre-commit because they were slow, and
|
|
7
7
|
// the documented consequence was that authors reached for --no-verify. A
|
|
8
8
|
// dispatcher that skips what it cannot do reproduces exactly that.
|
|
9
9
|
//
|
package/src/hooks/gates.mjs
CHANGED
|
@@ -100,7 +100,7 @@ export function getGate(name) {
|
|
|
100
100
|
* This is the load-bearing decision of the whole dispatcher. A gate that
|
|
101
101
|
* silently does nothing is indistinguishable from a gate that passed, and the
|
|
102
102
|
* documented consequence in this estate is an agent reaching for
|
|
103
|
-
* `--no-verify` -
|
|
103
|
+
* `--no-verify` - a repository removed its tests from pre-commit precisely
|
|
104
104
|
* because they were slow, and slow-plus-silent is what teaches everyone that
|
|
105
105
|
* hooks are advisory.
|
|
106
106
|
*/
|
package/src/hooks/where.mjs
CHANGED
|
@@ -55,7 +55,7 @@ export async function resolveHooksDir(root) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
// Rule 1: core.hooksPath wins. A relative value resolves against the top level
|
|
58
|
-
// of the working tree, which is what makes
|
|
58
|
+
// of the working tree, which is what makes a `hooks` directory work.
|
|
59
59
|
if (configured !== null) {
|
|
60
60
|
return {
|
|
61
61
|
dir: isAbsolute(configured) ? configured : resolve(top, configured),
|
package/src/identity.mjs
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// The per-developer git identity, as a file the repository does not track.
|
|
2
|
+
//
|
|
3
|
+
// A repository has more than one person in it, and each of them may already have an
|
|
4
|
+
// identity this tool has no business choosing. So the rules here are conservative in
|
|
5
|
+
// that order:
|
|
6
|
+
//
|
|
7
|
+
// 1. If `git.config` already exists, it is left exactly as it is. Not updated, not
|
|
8
|
+
// merged, not reformatted. It may have been written by a person, by an older
|
|
9
|
+
// version of this tool, or by hand years ago, and none of those are this tool's
|
|
10
|
+
// to second-guess.
|
|
11
|
+
// 2. Otherwise the identity comes from the repository's own last commit author.
|
|
12
|
+
// That is the person who has been working here, which is the best available
|
|
13
|
+
// answer to "who commits here" and requires no guessing: it is a fact, not a
|
|
14
|
+
// default.
|
|
15
|
+
// 3. Otherwise from the git identity configured for this machine or this user,
|
|
16
|
+
// which is what would have been used anyway had this file never existed.
|
|
17
|
+
// 4. Otherwise nothing is written. An identity nobody configured is not one to
|
|
18
|
+
// invent, and a file with a placeholder in it is worse than no file: git would
|
|
19
|
+
// use the placeholder.
|
|
20
|
+
//
|
|
21
|
+
// The file is deliberately NOT written into the repository's tracked files. It holds
|
|
22
|
+
// whoever is committing, and committing it publishes one developer's name and email
|
|
23
|
+
// to everybody else in the repository. `init` adds it to .gitignore when it creates
|
|
24
|
+
// one; a repository that already tracks it is left that way, because removing a
|
|
25
|
+
// tracked file is not something an adopting command should do silently.
|
|
26
|
+
|
|
27
|
+
import { execFile } from 'node:child_process';
|
|
28
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
29
|
+
import { join } from 'node:path';
|
|
30
|
+
import { promisify } from 'node:util';
|
|
31
|
+
|
|
32
|
+
const exec = promisify(execFile);
|
|
33
|
+
|
|
34
|
+
/** The file, at the repository root. Named for what it is: a git config fragment. */
|
|
35
|
+
export const IDENTITY_FILE = 'git.config';
|
|
36
|
+
|
|
37
|
+
/** The git config key that pulls the file in. */
|
|
38
|
+
export const INCLUDE_KEY = 'include.path';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run a git command and return trimmed stdout, or null if it fails.
|
|
42
|
+
*
|
|
43
|
+
* Null rather than an exception for every case: a repository that is not a git
|
|
44
|
+
* repository, a version of git that does not know a flag, a machine with no
|
|
45
|
+
* configured identity. None of those is an error worth refusing an adoption over.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} cwd
|
|
48
|
+
* @param {string[]} args
|
|
49
|
+
* @returns {Promise<string|null>}
|
|
50
|
+
*/
|
|
51
|
+
async function git(cwd, args) {
|
|
52
|
+
try {
|
|
53
|
+
const { stdout } = await exec('git', args, { cwd });
|
|
54
|
+
return stdout.trim();
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Who last committed here.
|
|
62
|
+
*
|
|
63
|
+
* `%an` and `%ae` rather than the configured identity on purpose: this is what
|
|
64
|
+
* actually happened, which is the answer to the question being asked. Reading
|
|
65
|
+
* `git config user.email` here would return the value this file is about to be
|
|
66
|
+
* derived from, and the step would be circular.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} root
|
|
69
|
+
* @returns {Promise<{name: string|null, email: string|null}>}
|
|
70
|
+
*/
|
|
71
|
+
export async function lastCommitAuthor(root) {
|
|
72
|
+
const line = await git(root, ['log', '-1', '--format=%an <%ae>']);
|
|
73
|
+
if (!line) return { name: null, email: null };
|
|
74
|
+
// The name may itself contain angle brackets, so the address is split off the
|
|
75
|
+
// LAST one rather than the first.
|
|
76
|
+
const at = line.lastIndexOf('<');
|
|
77
|
+
const close = line.lastIndexOf('>');
|
|
78
|
+
if (at === -1 || close < at) return { name: line || null, email: null };
|
|
79
|
+
return {
|
|
80
|
+
name: line.slice(0, at).trim() || null,
|
|
81
|
+
email: line.slice(at + 1, close).trim() || null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The identity this machine would use anyway.
|
|
87
|
+
*
|
|
88
|
+
* `--get` rather than `--global`: it resolves through system, global and local in
|
|
89
|
+
* git's own order, so what it returns is what a commit would have used had this file
|
|
90
|
+
* never been created. That is the fallback's whole point - it must not be a second
|
|
91
|
+
* opinion about what identity means.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} root
|
|
94
|
+
* @returns {Promise<{name: string|null, email: string|null}>}
|
|
95
|
+
*/
|
|
96
|
+
export async function configuredIdentity(root) {
|
|
97
|
+
const read = async (key) => {
|
|
98
|
+
const value = await git(root, ['config', '--get', key]);
|
|
99
|
+
return value && value !== '' ? value : null;
|
|
100
|
+
};
|
|
101
|
+
return { name: await read('user.name'), email: await read('user.email') };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The identity to write, and where it came from.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} root
|
|
108
|
+
* @returns {Promise<{name: string|null, email: string|null, from: 'last-commit'|'configured'|null,
|
|
109
|
+
* exists: boolean, bytes: number}>}
|
|
110
|
+
*/
|
|
111
|
+
export async function resolveIdentity(root) {
|
|
112
|
+
const path = join(root, IDENTITY_FILE);
|
|
113
|
+
try {
|
|
114
|
+
const info = await stat(path);
|
|
115
|
+
if (info.isFile()) {
|
|
116
|
+
return { name: null, email: null, from: null, exists: true, bytes: info.size };
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// Absent is the case this whole module exists for.
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const committed = await lastCommitAuthor(root);
|
|
123
|
+
if (committed.name && committed.email) {
|
|
124
|
+
return { ...committed, from: 'last-commit', exists: false, bytes: 0 };
|
|
125
|
+
}
|
|
126
|
+
const configured = await configuredIdentity(root);
|
|
127
|
+
if (configured.name || configured.email) {
|
|
128
|
+
return { ...configured, from: 'configured', exists: false, bytes: 0 };
|
|
129
|
+
}
|
|
130
|
+
return { name: null, email: null, from: null, exists: false, bytes: 0 };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The file's bytes.
|
|
135
|
+
*
|
|
136
|
+
* Only the keys that were found are written. A partial identity is written as a
|
|
137
|
+
* partial identity rather than padded with a placeholder, because a placeholder is
|
|
138
|
+
* an identity git would use.
|
|
139
|
+
*
|
|
140
|
+
* @param {{name: string|null, email: string|null}} identity
|
|
141
|
+
* @returns {string} LF-terminated
|
|
142
|
+
*/
|
|
143
|
+
export function renderIdentityFile({ name, email }) {
|
|
144
|
+
const lines = ['[user]'];
|
|
145
|
+
if (name) lines.push(`\tname = ${name}`);
|
|
146
|
+
if (email) lines.push(`\temail = ${email}`);
|
|
147
|
+
return `${lines.join('\n')}\n`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Whether `include.path` already names this repository's own file.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} root
|
|
154
|
+
* @returns {Promise<boolean>}
|
|
155
|
+
*/
|
|
156
|
+
export async function includesOwnIdentity(root) {
|
|
157
|
+
const values = (await git(root, ['config', '--local', '--get-all', INCLUDE_KEY])) ?? '';
|
|
158
|
+
return values.split('\n').map((v) => v.trim()).filter(Boolean).includes(`../${IDENTITY_FILE}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Point `core.hooksPath`-style local config at this repository's own identity file.
|
|
163
|
+
*
|
|
164
|
+
* `--replace-all` rather than `--add`, and that is the whole point: a value naming
|
|
165
|
+
* ANOTHER repository's identity file is not an addition to keep, it is a link to
|
|
166
|
+
* break. Two of the repositories here had exactly that, and both worked only because
|
|
167
|
+
* the sibling checkout happened to exist at the expected relative path.
|
|
168
|
+
*
|
|
169
|
+
* @param {string} root
|
|
170
|
+
* @returns {Promise<void>}
|
|
171
|
+
*/
|
|
172
|
+
export async function includeOwnIdentity(root) {
|
|
173
|
+
await git(root, ['config', '--local', '--replace-all', INCLUDE_KEY, `../${IDENTITY_FILE}`]);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Whether the repository tracks the identity file, which it should not.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} root
|
|
180
|
+
* @returns {Promise<boolean>}
|
|
181
|
+
*/
|
|
182
|
+
export async function tracksIdentity(root) {
|
|
183
|
+
const listed = await git(root, ['ls-files', '--error-unmatch', IDENTITY_FILE]);
|
|
184
|
+
return listed === IDENTITY_FILE;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Add the identity file to .gitignore, if it is not already there.
|
|
189
|
+
*
|
|
190
|
+
* Matching on the exact line rather than a substring, because a `.gitignore`
|
|
191
|
+
* entry of `/git.config` or `git.config` both mean it while a comment mentioning
|
|
192
|
+
* it means nothing, and appending a duplicate of an entry that is already present
|
|
193
|
+
* is how a file ends up listed twice.
|
|
194
|
+
*
|
|
195
|
+
* @param {string} root
|
|
196
|
+
* @returns {Promise<boolean>} whether the file was changed
|
|
197
|
+
*/
|
|
198
|
+
export async function ignoreIdentity(root) {
|
|
199
|
+
const path = join(root, '.gitignore');
|
|
200
|
+
let text;
|
|
201
|
+
try {
|
|
202
|
+
text = await readFile(path, 'utf8');
|
|
203
|
+
} catch {
|
|
204
|
+
text = '';
|
|
205
|
+
}
|
|
206
|
+
const already = text.split('\n').map((l) => l.trim()).includes(IDENTITY_FILE);
|
|
207
|
+
if (already) return false;
|
|
208
|
+
const prefix = text === '' || text.endsWith('\n') ? '' : '\n';
|
|
209
|
+
const block =
|
|
210
|
+
`${prefix}\n` +
|
|
211
|
+
'# Per-developer git identity, written by `simt init`. Not a file to share:\n' +
|
|
212
|
+
'# it carries whoever is committing into the tree, and a repository has more than\n' +
|
|
213
|
+
'# one of those.\n' +
|
|
214
|
+
`${IDENTITY_FILE}\n`;
|
|
215
|
+
const { writeFile } = await import('node:fs/promises');
|
|
216
|
+
await writeFile(path, text + block);
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
@@ -93,7 +93,7 @@ indent_style = tab`;
|
|
|
93
93
|
* every path as text and is the single most common way to corrupt a binary in a
|
|
94
94
|
* repository, because nothing tells git it is wrong. `auto` makes git decide per path
|
|
95
95
|
* from its own content sniffing, so a `.tif`, a `.zip` and a `.mc` file are left exactly
|
|
96
|
-
* as they are and only files that genuinely are text get normalised.
|
|
96
|
+
* as they are and only files that genuinely are text get normalised. One
|
|
97
97
|
* already worked this out on its own, which is the strongest evidence available that
|
|
98
98
|
* the conservative form is the one the estate would have picked.
|
|
99
99
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// the same rule restated in prose outside a generated region - prohibited by the
|
|
7
7
|
// anti-duplication doctrine, but not a contradiction.
|
|
8
8
|
//
|
|
9
|
-
// The distinction is not academic.
|
|
9
|
+
// The distinction is not academic. One AGENTS.md says "Do not use `uv run
|
|
10
10
|
// pytest`" - which contains the forbidden string `uv run pytest` verbatim. A naive
|
|
11
11
|
// substring scan reports that file as violating the very rule it is correctly
|
|
12
12
|
// restating. So every match is checked for prohibition context first.
|
|
@@ -397,7 +397,7 @@ function defaultId(root) {
|
|
|
397
397
|
* 1. A STALE GENERATED FIELD. `buildManifest` emits `$comment` and
|
|
398
398
|
* `projection.simtDevSha`; the repo owns everything else. `upgrade` spreads the
|
|
399
399
|
* existing manifest and refreshes only the SHA, so a field added to the writer
|
|
400
|
-
* after a repo was adopted never appears —
|
|
400
|
+
* after a repo was adopted never appears — one repository had been projecting for
|
|
401
401
|
* several versions with no `surfaces` key, and nothing said so. Every parse-based
|
|
402
402
|
* comparison passes, because the file is valid and the values in it are all true.
|
|
403
403
|
*
|
|
@@ -217,7 +217,7 @@ export function compose(body, { sha, sources = [] } = {}, style) {
|
|
|
217
217
|
);
|
|
218
218
|
const trimmed = body.replace(/\n+$/, '');
|
|
219
219
|
// Two lines here exist only because a real repo proved they are needed. A
|
|
220
|
-
// formatter that runs on commit -
|
|
220
|
+
// formatter that runs on commit - several repositories have one, and three of the
|
|
221
221
|
// other repos do too - rewrote the region on the very first commit: it inserted a
|
|
222
222
|
// blank line after the header comment and added the trailing newline. Both edits
|
|
223
223
|
// are things prettier considers correct, and either one alone makes `check`
|
|
@@ -226,7 +226,7 @@ export function compose(body, { sha, sources = [] } = {}, style) {
|
|
|
226
226
|
// projection emits the formatter's shape in the first place. The blank line before
|
|
227
227
|
// END is the same class: prettier treats an HTML comment after a paragraph as a
|
|
228
228
|
// new block and inserts the separating blank line itself, which made
|
|
229
|
-
//
|
|
229
|
+
// a `format:check` fail on a file whose generated region was correct.
|
|
230
230
|
const out =
|
|
231
231
|
trimmed === ''
|
|
232
232
|
? [s.begin, ...header, s.end]
|
|
@@ -293,7 +293,7 @@ export function inject(text, body, meta, opts = {}) {
|
|
|
293
293
|
// terminates the region with one; adding a second left every adopted file
|
|
294
294
|
// ending in a blank line. Nothing in this tool noticed, because `check`
|
|
295
295
|
// compares region bodies and the byte after END is not part of the region —
|
|
296
|
-
// but
|
|
296
|
+
// but a repository's own `git diff --check` gate did, and it is a gate
|
|
297
297
|
// that repo runs before every commit. The `updated` branch below has always
|
|
298
298
|
// been correct for exactly this reason.
|
|
299
299
|
//
|
package/src/projection/mdc.mjs
CHANGED
|
@@ -201,7 +201,7 @@ function stripFrontmatter(text) {
|
|
|
201
201
|
*
|
|
202
202
|
* An empty-string default is a deliberate "this repo has nothing to add here"
|
|
203
203
|
* and the line it sits on is DROPPED. That is how `preview-postflight` reaches
|
|
204
|
-
*
|
|
204
|
+
* a repository without printing an empty bullet, and how a repo with no
|
|
205
205
|
* production-domain note gets a clean document instead of a blank one.
|
|
206
206
|
*
|
|
207
207
|
* A placeholder with neither a declared nor a default value is a defect: the
|
|
@@ -218,7 +218,7 @@ export function renderMdc(rule, declared = {}) {
|
|
|
218
218
|
|
|
219
219
|
if (has && provided === '') {
|
|
220
220
|
// A repo declared this key and declared it empty: the line it sits on is
|
|
221
|
-
// removed. This is how preview-postflight reaches
|
|
221
|
+
// removed. This is how preview-postflight reaches a repository without
|
|
222
222
|
// printing a blank bullet.
|
|
223
223
|
body = dropOrReplace(body, sub.key, '');
|
|
224
224
|
continue;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
//
|
|
18
18
|
// 1. A POINTER IS NOT A COPY. `CLAUDE.md` and `GEMINI.md` are symlinks to
|
|
19
19
|
// `AGENTS.md`. A copy is the corpus's one true content duplicate
|
|
20
|
-
// (
|
|
20
|
+
// (one repository's `CLAUDE.md` is byte-identical to its `AGENTS.md`), and
|
|
21
21
|
// the DeepSeek harness explicitly rejects loading both in one directory
|
|
22
22
|
// because "repositories in transition commonly duplicate guidance" — and
|
|
23
23
|
// *its* dedup is content-based, so a symlink collapses to one while a
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
// mode bit. So those get machine-written copies — and because every byte is
|
|
33
33
|
// derived from `rules/mdc/*.mdc` and `simt-dev check` compares each against
|
|
34
34
|
// that same derivation, they are not duplication, they are a build output.
|
|
35
|
-
// The difference from
|
|
35
|
+
// The difference from that duplicate is that nobody can hand-edit one
|
|
36
36
|
// and have it survive a sync.
|
|
37
37
|
//
|
|
38
38
|
// 3. AN ALWAYS-ON RULE EMITS NO SCOPING FRONTMATTER. Claude Code's `paths:` key
|
|
@@ -41,7 +41,7 @@ const MARKERS = [
|
|
|
41
41
|
/**
|
|
42
42
|
* Extensions are weaker evidence than marker files: a stray script must not
|
|
43
43
|
* define a repo's stack. `python` is deliberately absent here because
|
|
44
|
-
*
|
|
44
|
+
* a repository carries .py tooling scripts inside a Gradle project - a marker file
|
|
45
45
|
* is required for that tag. `kotlin` has no marker file, so it stays here.
|
|
46
46
|
*/
|
|
47
47
|
const EXTENSION_TAGS = new Map([
|
|
@@ -83,7 +83,7 @@ const DEP_TAGS = new Map([
|
|
|
83
83
|
*
|
|
84
84
|
* Three, and the reason is a convention rather than a preference: the Android Gradle
|
|
85
85
|
* Plugin puts a module's manifest at `<module>/src/main/AndroidManifest.xml`, and
|
|
86
|
-
*
|
|
86
|
+
* one common layout sits at `app/src/main/AndroidManifest.xml`. A bound shallower than the
|
|
87
87
|
* convention makes `has-android-manifest` the shape S07 found - a fact no repository
|
|
88
88
|
* could ever have. Deeper would be a scan rather than a probe, and ADR-013's promise
|
|
89
89
|
* is that adding an ecosystem is data, so a fact must not cost a search.
|
|
@@ -300,7 +300,7 @@ export async function fingerprint(root) {
|
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
-
//
|
|
303
|
+
// two repositories both carry a wear/ module; only one of them is the wearable.
|
|
304
304
|
if (names.has('wear') && (await isDir(join(root, 'wear')))) {
|
|
305
305
|
stack.add('wear-os');
|
|
306
306
|
evidence.push('wear/');
|
package/src/resolve/source.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
//
|
|
9
9
|
// The second location is the one nobody wrote down: **the repository being operated
|
|
10
10
|
// on is itself a location for a declared source**. Someone who cloned only
|
|
11
|
-
//
|
|
11
|
+
// one repository has its body on disk at `.agents/skills/slice-commit`
|
|
12
12
|
// with no workspace anywhere. The registry entry is satisfied by that file.
|
|
13
13
|
//
|
|
14
14
|
// Bytes are what make this safe rather than hopeful. Every source declares its size,
|
package/src/tsconfig.mjs
CHANGED
|
@@ -32,7 +32,7 @@ export const TSCONFIG_FILE = 'tsconfig.json';
|
|
|
32
32
|
* The strictness floor.
|
|
33
33
|
*
|
|
34
34
|
* Every key here is already set, and set the same way, by every `tsconfig.json` in the
|
|
35
|
-
* estate that could afford it.
|
|
35
|
+
* estate that could afford it. One repository is the outlier and it is not one on purpose: it
|
|
36
36
|
* is missing the seven flags below that the three social repos share, which is the gap
|
|
37
37
|
* plan section 3.1 row 5 named. Nothing in this object is a value anybody has argued about;
|
|
38
38
|
* it is the intersection of what four repositories independently wrote.
|