archstrict 0.0.0 → 0.1.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/.agents/hooks/hooks.json +29 -0
- package/.agents/hooks/post-tool-use.mjs +107 -0
- package/.agents/hooks/pre-tool-use.mjs +182 -0
- package/.agents/mcp/server.mjs +71 -0
- package/.agents/plugin.json +19 -0
- package/AGENTS.md +69 -0
- package/CHANGELOG.md +38 -0
- package/README.ja.md +62 -0
- package/README.md +63 -2
- package/dist/augmentation-cache.js +65 -0
- package/dist/check-options.js +40 -0
- package/dist/classify.js +148 -0
- package/dist/cli.js +239 -0
- package/dist/config-pointer.js +251 -0
- package/dist/config.js +186 -0
- package/dist/edge-cache.js +530 -0
- package/dist/mcp-server.js +111 -0
- package/dist/module-candidates.js +118 -0
- package/dist/module-graph.js +2072 -0
- package/dist/project-path.js +59 -0
- package/dist/report-error.js +13 -0
- package/dist/rules/config-meaning.js +143 -0
- package/dist/rules/constraints.js +417 -0
- package/dist/rules/cycles.js +257 -0
- package/dist/rules/deprecated.js +67 -0
- package/dist/rules/empty-rule.js +101 -0
- package/dist/rules/moves.js +79 -0
- package/dist/rules/must-be-empty.js +52 -0
- package/dist/rules/public-surface.js +100 -0
- package/dist/rules/type-leak.js +562 -0
- package/dist/rules/uncovered.js +75 -0
- package/dist/todo-migration.js +112 -0
- package/dist/todo-store.js +434 -0
- package/dist/type-closure.js +959 -0
- package/dist/verbs/agents.js +116 -0
- package/dist/verbs/check.js +957 -0
- package/dist/verbs/fix.js +170 -0
- package/dist/verbs/hotspots.js +261 -0
- package/dist/verbs/init.js +522 -0
- package/dist/verbs/recommend.js +800 -0
- package/dist/verbs/rules.js +188 -0
- package/dist/verbs/search.js +109 -0
- package/dist/verbs/simulate.js +220 -0
- package/dist/verbs/todo.js +163 -0
- package/dist/warm-graph.js +82 -0
- package/docs/boundary-patterns.md +374 -0
- package/docs/calibrated-rules-design.md +124 -0
- package/docs/init-singleton-modules.md +128 -0
- package/docs/maintenance.md +82 -0
- package/docs/releasing.md +55 -0
- package/docs/rules-edge-cache.md +50 -0
- package/docs/todo-single-file-migration.md +58 -0
- package/llms.txt +19 -0
- package/package.json +57 -4
- package/skills/archstrict/SKILL.md +42 -0
- package/skills/archstrict/references/agents-verb.md +39 -0
- package/skills/archstrict/references/config.md +107 -0
- package/skills/archstrict/references/hook.md +57 -0
- package/skills/archstrict/references/path-rules.md +57 -0
- package/skills/archstrict/references/patterns.md +883 -0
- package/skills/archstrict/references/prove-rules.md +58 -0
- package/skills/archstrict/references/rearchitect.md +35 -0
- package/skills/archstrict/references/recommend.md +80 -0
- package/skills/archstrict/references/rules.md +146 -0
- package/skills/archstrict/references/simulate.md +109 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Responsibility: turn a list of analyzed files into the groups init
|
|
2
|
+
// declares as modules - one directory group per top-level (or per-container)
|
|
3
|
+
// directory that holds an analyzed file, one file group per loose file -
|
|
4
|
+
// plus the on-disk naming rule for each group and the literal
|
|
5
|
+
// declaredModules-entry text init pastes into the generated config. The
|
|
6
|
+
// same grouping and naming also produces the paste-ready suggestion for a
|
|
7
|
+
// file an EXISTING config doesn't cover yet (rule 3's own `do:`, `archstrict
|
|
8
|
+
// rules <path>`, and init's re-run listing all share `suggestUncovered`
|
|
9
|
+
// below, so the three can never drift into different phrasings of the same
|
|
10
|
+
// entry).
|
|
11
|
+
// Boundary: no I/O, and no opinion about WHICH files are analyzed - the
|
|
12
|
+
// caller (init's own walk, or a config-vs-graph consistency check) already
|
|
13
|
+
// decided that and hands this module the resulting file list, anchor set,
|
|
14
|
+
// and the config's own declaredModules (for the `taken`-name check).
|
|
15
|
+
import { moduleGlobBaseDir } from "./module-graph.js";
|
|
16
|
+
const byteSort = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
17
|
+
// Groups analyzed files by their nearest anchor: for a file `f`, the
|
|
18
|
+
// longest anchor that contains it. A file sitting directly in its anchor is
|
|
19
|
+
// its own group (a file group); otherwise the group is the first directory
|
|
20
|
+
// below the anchor on the path to `f` (a directory group) - so a deeply
|
|
21
|
+
// nested file (`src/runtime/db/pool.ts`) still groups under `src/runtime`,
|
|
22
|
+
// one entry per top-level directory, not one per leaf.
|
|
23
|
+
export function groupAnalyzedFiles(files, anchors) {
|
|
24
|
+
const sortedAnchors = [...anchors].sort((a, b) => b.length - a.length);
|
|
25
|
+
const groups = new Map();
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
// "" is always the last (shortest) anchor tried, and always matches -
|
|
28
|
+
// every analyzed file is under the project root - so this always finds one.
|
|
29
|
+
const anchor = sortedAnchors.find((a) => a === "" || file.startsWith(`${a}/`));
|
|
30
|
+
const rest = anchor === "" ? file : file.slice(anchor.length + 1);
|
|
31
|
+
const [first, ...more] = rest.split("/");
|
|
32
|
+
const rel = anchor === "" ? first : `${anchor}/${first}`;
|
|
33
|
+
const kind = more.length === 0 ? "file" : "dir";
|
|
34
|
+
const existing = groups.get(rel);
|
|
35
|
+
if (existing !== undefined) {
|
|
36
|
+
existing.fileCount++;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
groups.set(rel, { kind, rel, anchor, onDiskName: first, fileCount: 1 });
|
|
40
|
+
}
|
|
41
|
+
return [...groups.values()].sort((a, b) => byteSort(a.rel, b.rel));
|
|
42
|
+
}
|
|
43
|
+
// Naming, three cases: (1) a group's name is its on-disk name, unless that
|
|
44
|
+
// name collides with another group's own on-disk name at the SAME anchor
|
|
45
|
+
// depth - one directory cannot hold a file and a directory of the same
|
|
46
|
+
// name, so a top-level `cli.ts` and a `src/cli.ts` never collide with each
|
|
47
|
+
// other directly, only through rule 2; (2) a group below the project root
|
|
48
|
+
// whose on-disk name is already `taken` (by an existing config entry, or by
|
|
49
|
+
// another group sharing that name) instead takes its own project-relative
|
|
50
|
+
// path as its name; (3) a project-root group (anchor "") has no deeper
|
|
51
|
+
// path to fall back to - its on-disk name IS its rel - so a root name still
|
|
52
|
+
// `taken` after that takes a "./"-prefixed rel instead (measured: pasting
|
|
53
|
+
// `{ name: "./tools", glob: "tools/**" }` next to an existing "tools" gave
|
|
54
|
+
// 0 violations and tsc passed). This only fires for a re-run's or rule 3's
|
|
55
|
+
// suggestion - a fresh init never has a `taken` set with anything a fresh
|
|
56
|
+
// root group's own on-disk name could collide with.
|
|
57
|
+
export function nameCandidates(groups, taken) {
|
|
58
|
+
const onDiskCounts = new Map();
|
|
59
|
+
for (const g of groups)
|
|
60
|
+
onDiskCounts.set(g.onDiskName, (onDiskCounts.get(g.onDiskName) ?? 0) + 1);
|
|
61
|
+
return groups.map((g) => {
|
|
62
|
+
const collides = taken.has(g.onDiskName) || (g.anchor !== "" && (onDiskCounts.get(g.onDiskName) ?? 0) > 1);
|
|
63
|
+
const name = g.anchor === "" ? (collides ? `./${g.rel}` : g.onDiskName) : collides ? g.rel : g.onDiskName;
|
|
64
|
+
const glob = g.kind === "dir" ? `${g.rel}/**` : g.rel;
|
|
65
|
+
const entry = g.kind === "dir" ? { name, glob } : { name, glob, surface: g.onDiskName };
|
|
66
|
+
return { ...g, entry, excludeGlob: glob };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const q = JSON.stringify;
|
|
70
|
+
// The literal declaredModules[] entry text init pastes into the generated
|
|
71
|
+
// config, and the same text a later suggestion (for the root-name-collision
|
|
72
|
+
// case above) would paste for an uncovered path - kept as one function so
|
|
73
|
+
// the two can never drift into two different phrasings of the same entry
|
|
74
|
+
// shape. A directory entry carries no `surface` of its own (the top-level
|
|
75
|
+
// default, or the directory's own package.json exports map, applies
|
|
76
|
+
// instead) - a file
|
|
77
|
+
// entry always does, naming the file itself, since a file with no surface
|
|
78
|
+
// of its own would otherwise be entirely private (nothing else could ever
|
|
79
|
+
// export from it).
|
|
80
|
+
export function declaredModuleEntryText(entry) {
|
|
81
|
+
return entry.surface === undefined
|
|
82
|
+
? `{ name: ${q(entry.name)}, glob: ${q(entry.glob)} }`
|
|
83
|
+
: `{ name: ${q(entry.name)}, glob: ${q(entry.glob)}, surface: ${q(entry.surface)} }`;
|
|
84
|
+
}
|
|
85
|
+
// The one shared entry point rule 3, `archstrict rules <path>`, and init's
|
|
86
|
+
// re-run all call: given the project-relative paths of files an EXISTING
|
|
87
|
+
// config's declaredModules doesn't cover, group and name them exactly as a
|
|
88
|
+
// fresh init would, with the config's own declaredModules entries counted
|
|
89
|
+
// as `taken` names. Anchors are the project root plus the parent directory
|
|
90
|
+
// of each existing entry's own glob base - the same depth a fresh init
|
|
91
|
+
// itself would have grouped that entry at, computed by string ops alone
|
|
92
|
+
// (moduleGlobBaseDir already strips the glob down to its literal prefix;
|
|
93
|
+
// only its own parent directory is needed here, not whether that prefix
|
|
94
|
+
// names a real file or directory on disk).
|
|
95
|
+
export function suggestUncovered(uncoveredRelFiles, declaredModules) {
|
|
96
|
+
const anchors = new Set([""]);
|
|
97
|
+
for (const dm of declaredModules) {
|
|
98
|
+
const base = moduleGlobBaseDir(dm.glob);
|
|
99
|
+
const slash = base.lastIndexOf("/");
|
|
100
|
+
anchors.add(slash === -1 ? "" : base.slice(0, slash));
|
|
101
|
+
}
|
|
102
|
+
const taken = new Set(declaredModules.map((dm) => dm.name));
|
|
103
|
+
return nameCandidates(groupAnalyzedFiles(uncoveredRelFiles, [...anchors]), taken);
|
|
104
|
+
}
|
|
105
|
+
// Which of `suggestUncovered`'s own groups a single project-relative file
|
|
106
|
+
// belongs to - a file group's own `rel` IS the file, a directory group's
|
|
107
|
+
// `rel` is its own directory, so the file sits somewhere below it.
|
|
108
|
+
export function groupForRelFile(rel, groups) {
|
|
109
|
+
return groups.find((g) => g.rel === rel || rel.startsWith(`${g.rel}/`));
|
|
110
|
+
}
|
|
111
|
+
// The one sentence rule 3's `do:` and `archstrict rules <path>` both print
|
|
112
|
+
// for a single uncovered file - the paste-ready entry, with the exclude
|
|
113
|
+
// alternative right beside it so the same suggestion never leads an agent
|
|
114
|
+
// to add a directory entry for a file that turns out not to be module
|
|
115
|
+
// content at all.
|
|
116
|
+
export function suggestionDoText(group) {
|
|
117
|
+
return `add ${declaredModuleEntryText(group.entry)} to declaredModules in archstrict.config.ts, or add ${q(group.excludeGlob)} to exclude if it is not module content; then run archstrict init`;
|
|
118
|
+
}
|