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,124 @@
|
|
|
1
|
+
archstrict provides the built-in `config-meaning` assessment through `archstrict check --prove`. Jev chooses whether a rule's shape agrees with or contradicts its `because` text. A contradiction produces a finding when confidence reaches 0.7. A lower confidence produces an `undecided` result. A consistent choice produces no finding. This check is advisory and cannot enter todo. The general, user-authored `calibrated: [...]` array remains a design only. Neither `check` nor `rules <path>` reads that array. Adding it to a real config has no effect.
|
|
2
|
+
|
|
3
|
+
# Calibrated rules
|
|
4
|
+
|
|
5
|
+
archstrict reports the class of evidence for each result.
|
|
6
|
+
The report keeps the three tiers distinct.
|
|
7
|
+
This design adds an optional tier that uses Jev from TypeSafe AI.
|
|
8
|
+
|
|
9
|
+
| Tier | What the result establishes |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| Proven | Existing structural rules produce deterministic results that a consumer can replay. |
|
|
12
|
+
| Calibrated | A trained probability describes accuracy across many cases. It never guarantees correctness for one case. |
|
|
13
|
+
| Explored | An agent inspects the code and records the date. The result has no repeatability guarantee. |
|
|
14
|
+
|
|
15
|
+
This document defines only the calibrated tier.
|
|
16
|
+
The explored tier belongs to separate future work and stays outside this design.
|
|
17
|
+
|
|
18
|
+
Do not use a calibrated score to rank, annotate, or change the weight of a proven violation.
|
|
19
|
+
Such a score invites a consumer to discount a real violation.
|
|
20
|
+
The calibrated tier produces separate results with explicit labels and leaves deterministic results unchanged.
|
|
21
|
+
|
|
22
|
+
## 1. Config location
|
|
23
|
+
|
|
24
|
+
The new `calibrated` field contains an array of rule entries at the top level of the config.
|
|
25
|
+
It sits beside `edges` and `declaredModules`; it never sits inside `edges`.
|
|
26
|
+
|
|
27
|
+
Each entry has the shape `{ tags | glob, ask, criteria, at, because }`.
|
|
28
|
+
The `at` field is optional.
|
|
29
|
+
Each entry selects files with either `tags` or `glob`, never both.
|
|
30
|
+
These alternatives follow the existing approach to file selection through classification.
|
|
31
|
+
|
|
32
|
+
| Field | Purpose |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| `tags` | Select the files through their tags. |
|
|
35
|
+
| `glob` | Select the files through a path pattern. |
|
|
36
|
+
| `ask` | State the question that Jev assesses for each selected file. |
|
|
37
|
+
| `criteria` | State the criteria for that assessment. |
|
|
38
|
+
| `at` | Set the optional confidence threshold for a violation. |
|
|
39
|
+
| `because` | State why the rule exists. |
|
|
40
|
+
|
|
41
|
+
Every entry requires `because`, as other root-level rules require a reason.
|
|
42
|
+
|
|
43
|
+
## 2. Activation
|
|
44
|
+
|
|
45
|
+
The tier requires the `TYPESAFE_API_KEY` environment variable before it evaluates rules.
|
|
46
|
+
When the key is absent, `check` reports every configured calibrated rule as an individual, named `skipped` entry.
|
|
47
|
+
Each skipped entry carries the rule id, its configured `because`, and the reason `TYPESAFE_API_KEY is absent`.
|
|
48
|
+
The report must retain these entries rather than omit them.
|
|
49
|
+
|
|
50
|
+
This reuses the principle of rule 4, `empty-rule-set`: a rule that checks nothing must not look like a pass.
|
|
51
|
+
|
|
52
|
+
The built-in `config-meaning` check is a deliberate, stricter exception to this general activation rule.
|
|
53
|
+
It also requires `--prove` as explicit permission for each invocation.
|
|
54
|
+
A routine CI or pre-commit check must not make paid requests because an unrelated tool has set the same credential.
|
|
55
|
+
Without `--prove`, this check produces no assessment and makes no request.
|
|
56
|
+
With `--prove`, a missing key or a failed batch produces one `skipped` entry for the whole batch.
|
|
57
|
+
A contradiction with confidence below 0.7 produces an `undecided` entry with the rule's own `because` text.
|
|
58
|
+
Its evidence includes the returned confidence and probabilities, but it has no `confidence` field.
|
|
59
|
+
A contradiction at or above 0.7 produces a finding with a `confidence` field.
|
|
60
|
+
All three outcomes carry `tier: "calibrated"`; they remain advisory and cannot enter todo, including in modules marked `strict`.
|
|
61
|
+
The general field, threshold, and todo design below applies to future user-authored rules.
|
|
62
|
+
|
|
63
|
+
An exhaustive allow list does not require a semantic assessment.
|
|
64
|
+
Static config validation alone cannot establish the set of real values, but the dependency graph supplies those values.
|
|
65
|
+
A deterministic graph check can detect an exhaustive allow list; the claim that static analysis cannot catch this case was incorrect.
|
|
66
|
+
The built-in assessment instead checks whether the configured shape contradicts its own reason.
|
|
67
|
+
|
|
68
|
+
## 3. Violation shape
|
|
69
|
+
|
|
70
|
+
Each calibrated result carries the required fields `rule`, `path`, `line`, `column`, `evidence`, `because`, `config`, and `do`.
|
|
71
|
+
The `config` field identifies the assessed edge-rule entry as structured data.
|
|
72
|
+
It also carries two fields that deterministic violations do not carry:
|
|
73
|
+
|
|
74
|
+
| Field | Value |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `confidence` | The model's own calibrated number, from 0 to 1. |
|
|
77
|
+
| `tier` | The literal string `"calibrated"`. |
|
|
78
|
+
|
|
79
|
+
A consumer can filter or sort results by `tier` without parsing text to identify their class of evidence.
|
|
80
|
+
A confidence value describes the calibrated result alone; it never modifies a deterministic violation.
|
|
81
|
+
|
|
82
|
+
## 4. Threshold and exit code
|
|
83
|
+
|
|
84
|
+
The report includes every calibrated result, including those that cannot affect the exit code.
|
|
85
|
+
The rule's own `at` field determines whether its result can block a check.
|
|
86
|
+
|
|
87
|
+
| Config and confidence | Report and todo | Effect on `check` |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| `at` is absent | Report every result with `tier: "calibrated"`. Each result is eligible for todo. | Advisory only. The result never affects the exit code. |
|
|
90
|
+
| `at` is present and `confidence >= at` | Report the result as a violation, subject to the existing todo rules. | Treat it like a deterministic violation for the exit code and the hard gate in a `strict` module. |
|
|
91
|
+
| `at` is present and `confidence < at` | Report the result as information. | The result does not block the check or affect its exit code. |
|
|
92
|
+
|
|
93
|
+
The design requires two independent choices.
|
|
94
|
+
The API key enables evaluation; the `at` field permits a result to block a check.
|
|
95
|
+
The advisory default does not grant that second permission.
|
|
96
|
+
|
|
97
|
+
## 5. Todo eligibility and identity
|
|
98
|
+
|
|
99
|
+
Calibrated results are eligible for todo under the existing rules for module debt.
|
|
100
|
+
The fingerprint hashes `rule`, `path`, and a hash of the `ask` text itself.
|
|
101
|
+
It never includes `confidence`.
|
|
102
|
+
This identity freezes the question, not the answer that the model returns.
|
|
103
|
+
|
|
104
|
+
A model update can change a score without changing that identity.
|
|
105
|
+
A score change alone must not unfreeze or refreeze existing debt.
|
|
106
|
+
A change to the `ask` text changes the identity because it changes the question.
|
|
107
|
+
|
|
108
|
+
This reuses the existing pattern for the `type-leak` fingerprint.
|
|
109
|
+
That fingerprint removes the mutable `referenced by` suffix from the evidence before it computes the hash.
|
|
110
|
+
Both fingerprints exclude details that can vary while the debt stays the same.
|
|
111
|
+
|
|
112
|
+
## 6. Interaction with strict modules
|
|
113
|
+
|
|
114
|
+
Apply the threshold and exit behavior in decision 4 to modules marked `strict`.
|
|
115
|
+
That table defines the interaction; this tier adds no separate condition for those modules.
|
|
116
|
+
|
|
117
|
+
## 7. Projection through `rules <path>`
|
|
118
|
+
|
|
119
|
+
When a calibrated rule's `tags` or `glob` matches the queried path, `rules <path>` shows it in a new section.
|
|
120
|
+
The section shows the rule's `ask` text and its configured `at` threshold, if present.
|
|
121
|
+
It never shows a score because a query before an edit does not evaluate the future code.
|
|
122
|
+
|
|
123
|
+
This projection gives an agent the question before the agent writes a line.
|
|
124
|
+
It follows the same principle as the existing `allowDeny`, `order`, and `point` projections.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# init declares one module per directory or loose file
|
|
2
|
+
|
|
3
|
+
`archstrict init [dir]` walks the project with check's own file-eligibility
|
|
4
|
+
rule and declares one module per top-level directory that holds an
|
|
5
|
+
analyzed source file (`.ts`, `.tsx`, `.mts`, or `.cts`), and one
|
|
6
|
+
single-file module per loose top-level file of one of those extensions -
|
|
7
|
+
both inside the opened container (`src/` by default) and at the
|
|
8
|
+
project root. Every file the first `check` analyzes then belongs to
|
|
9
|
+
exactly one declared module, by construction. The first check therefore
|
|
10
|
+
reports 0 `uncovered-module` and 0 `empty-rule-set` violations, on any
|
|
11
|
+
project shape.
|
|
12
|
+
|
|
13
|
+
## The problem this replaces
|
|
14
|
+
|
|
15
|
+
Before this change, `init` declared a module for each directory under a
|
|
16
|
+
fixed glob (`src/*`) and excluded every loose top-level `.ts` file with a
|
|
17
|
+
literal `exclude: ["*.ts"]`. Two real shapes broke that:
|
|
18
|
+
|
|
19
|
+
- A directory with no `src/` at all, or a `src/` holding no directories
|
|
20
|
+
(only loose files) - `init` had nothing to declare, or refused.
|
|
21
|
+
- A directory alongside `src/` at the project root, or a loose file
|
|
22
|
+
directly inside `src/` itself - each showed up as its own
|
|
23
|
+
`uncovered-module` violation on the very first `check`, which `todo`
|
|
24
|
+
can never freeze away (an `uncovered-module` violation has no module
|
|
25
|
+
directory to freeze it into). A project in this shape stayed at exit 1
|
|
26
|
+
permanently unless a person hand-wrote an `exclude` entry for those
|
|
27
|
+
files before running `todo` for the first time.
|
|
28
|
+
|
|
29
|
+
## The decision
|
|
30
|
+
|
|
31
|
+
Each analyzed file becomes its own module (a directory module for a
|
|
32
|
+
directory's files, a single-file module - `surface` naming the file
|
|
33
|
+
itself - for a loose file), never a shared catch-all covering every loose
|
|
34
|
+
file at once. Two ideas were rejected:
|
|
35
|
+
|
|
36
|
+
- **Printing an inventory of directories, with loose files left
|
|
37
|
+
uncovered.** This reproduces the exact trap above: the first check
|
|
38
|
+
still reports `uncovered-module`, and it still can't be frozen.
|
|
39
|
+
- **One catch-all module for every loose file at once
|
|
40
|
+
(`{ name: "loose", glob: "src/*.ts" }`).** Its own public surface is
|
|
41
|
+
every one of those files' own exports at once - a degenerate shape with
|
|
42
|
+
no real public/private distinction inside it. `init` also never writes
|
|
43
|
+
a glob whose base is the project root (`**`, `*.ts`): such a module's
|
|
44
|
+
directory is the whole project.
|
|
45
|
+
|
|
46
|
+
A directory module carries no per-entry `surface` of its own - the
|
|
47
|
+
project's own top-level default applies, or a real `package.json`
|
|
48
|
+
`exports` map at that directory's own root, when it has one. Setting a
|
|
49
|
+
per-entry `surface` would turn that derivation off for every directory
|
|
50
|
+
`init` ever declares.
|
|
51
|
+
|
|
52
|
+
## Naming
|
|
53
|
+
|
|
54
|
+
A group's name is its on-disk name (the directory's name, or the file's
|
|
55
|
+
name including its extension) - one directory cannot hold a file and
|
|
56
|
+
another directory of the same name, so this alone never collides at the
|
|
57
|
+
same level. A group below the project root whose on-disk name is already
|
|
58
|
+
taken (by a top-level entry of the same name) instead takes its own
|
|
59
|
+
project-relative path as its name: a root `cli.ts` and a container's own
|
|
60
|
+
`cli.ts` become `"cli.ts"` and, say, `"src/cli.ts"`.
|
|
61
|
+
|
|
62
|
+
## Everything the walk excludes
|
|
63
|
+
|
|
64
|
+
`init` writes an `exclude` covering: its own two files
|
|
65
|
+
(`archstrict.config.ts`, `archstrict.types.ts`); every hidden directory,
|
|
66
|
+
at any depth (`.git`, a tool's own state directory) - the same thing
|
|
67
|
+
`tsc`'s own default `include` already skips; a fixed list of common
|
|
68
|
+
non-source directory names (`test`, `tests`, `example`, `examples`,
|
|
69
|
+
`spike`, `build`, `coverage`, `fixtures`, `e2e`, `tmp`), each added only when a
|
|
70
|
+
real directory of that name exists on disk; and a colocated test file's own
|
|
71
|
+
naming convention (`*.test.ts`, `*.spec.tsx`, and every other suffix/analyzed-
|
|
72
|
+
extension pair, plus a `__tests__/` directory), each added only when a real
|
|
73
|
+
analyzed file already matches it. A container named on the command line is
|
|
74
|
+
never treated as noise, even when its own name is on the directory list.
|
|
75
|
+
The two hidden-directory patterns are the same on every machine (a
|
|
76
|
+
committed config never names a directory that exists on only one
|
|
77
|
+
machine); a directory or test file a project decided is real source keeps
|
|
78
|
+
its own exclude entry removed by hand - the config comment names the
|
|
79
|
+
reason (a test file imports across modules as a fixture, and boundary
|
|
80
|
+
rules read production code) right beside the pattern it excluded.
|
|
81
|
+
|
|
82
|
+
## Zero-directory and zero-candidate cases
|
|
83
|
+
|
|
84
|
+
A container holding only loose files (no directories at all) still
|
|
85
|
+
declares one module per file; `init` prints one extra line, naming the
|
|
86
|
+
alternative (checking the whole container as one module instead) without
|
|
87
|
+
writing it - that shape stays a hand-edit, since a `do:` line names one
|
|
88
|
+
action, not a menu.
|
|
89
|
+
|
|
90
|
+
A project with no analyzed source file anywhere - under the seeded
|
|
91
|
+
exclude - has nothing for `init` to declare. It writes neither file and
|
|
92
|
+
exits 1, naming what's missing.
|
|
93
|
+
|
|
94
|
+
## Re-run
|
|
95
|
+
|
|
96
|
+
`init` never touches an existing `archstrict.config.ts`. A re-run only
|
|
97
|
+
re-reads it and rewrites `archstrict.types.ts` (the `ModuleName` union)
|
|
98
|
+
from its own `declaredModules` names - never from a fresh walk. A
|
|
99
|
+
directory argument on a re-run is still validated (the same syntax rules
|
|
100
|
+
apply), then ignored: it only ever chose a container for a config `init`
|
|
101
|
+
is about to write.
|
|
102
|
+
|
|
103
|
+
## What stays out of this change
|
|
104
|
+
|
|
105
|
+
A re-run does not yet print the paste-ready `declare:`/`exclude:` lines
|
|
106
|
+
for a file that has since fallen outside every declared module (only the
|
|
107
|
+
`ModuleName` regeneration lands here); rule 3's own suggested fix text and
|
|
108
|
+
`check`'s own footer are unchanged for the same reason.
|
|
109
|
+
|
|
110
|
+
`init --json` and `recommend`'s own directory argument landed in a later
|
|
111
|
+
change: `recommend [dir]` without a config now runs this same walk in
|
|
112
|
+
memory (no file is written) instead of a single-level modules-glob
|
|
113
|
+
discovery, so its own proposed globs always agree with what `init` would
|
|
114
|
+
write for the same tree. `init --json` reports one object on success
|
|
115
|
+
(`configPath`, `typesPath`, `configWritten`, `opened`, `moduleNames`,
|
|
116
|
+
`hiddenDirs`, `noiseDirs`, `testFileExcludes`, `uncovered`, `notes`, `do`)
|
|
117
|
+
and `{ error, do }`
|
|
118
|
+
on failure, the same convention `check --json` and `todo --json` already
|
|
119
|
+
follow. A later change removed the old single-level discovery path
|
|
120
|
+
entirely, once no user-facing command called it anymore: `declaredModules`
|
|
121
|
+
is now the only way a module is ever bounded.
|
|
122
|
+
|
|
123
|
+
Rule 6 (type-leak) can still report a finding for a singleton file whose
|
|
124
|
+
own export structurally exposes another module's own internal
|
|
125
|
+
declaration - this is this project's own currently-implemented reading of
|
|
126
|
+
that rule, unrelated to this change and unaffected by it: declaring more,
|
|
127
|
+
smaller modules only means more module boundaries between files for that
|
|
128
|
+
existing rule to evaluate, not a new rule or a new finding class.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Maintenance
|
|
2
|
+
|
|
3
|
+
## Checks
|
|
4
|
+
|
|
5
|
+
Run the main checks before a release or after a code change:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm run build
|
|
9
|
+
npm run typecheck
|
|
10
|
+
npm test
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Run the dogfood scenario and the oracle comparisons separately:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm run dogfood:nukadoko
|
|
17
|
+
node scripts/run-prisma-oracle.mjs <path-to-a-real-prisma/prisma-clone>
|
|
18
|
+
node scripts/run-vscode-oracle.mjs <path-to-a-real-microsoft/vscode-clone>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`dogfood:nukadoko` runs the built CLI's full init/check/todo/edit/check round trip against a real,
|
|
22
|
+
unrelated published `src/` copied into a disposable scratch directory. The two oracle scripts
|
|
23
|
+
re-run the Prisma and VS Code boundary-config comparisons through the real, built CLI against a
|
|
24
|
+
scratch copy of a local clone, converting each project's own real config with the existing
|
|
25
|
+
`scripts/convert-*` converters. Neither writes into the clone itself. See
|
|
26
|
+
[AGENTS.md](../AGENTS.md)'s Commands section for what each requires (a local clone, `pnpm install`
|
|
27
|
+
for the Prisma one) and what each prints.
|
|
28
|
+
|
|
29
|
+
`npm run ci:ts7-probe` measures which of the operations rule 6 needs work on whatever typescript 7
|
|
30
|
+
happens to be installed. It never fails; an unsupported operation is the measurement, not an error.
|
|
31
|
+
archstrict itself always analyzes with its own pinned `typescript` dependency, independent of this
|
|
32
|
+
probe.
|
|
33
|
+
|
|
34
|
+
## Dependencies and Node.js
|
|
35
|
+
|
|
36
|
+
Do not add a dependency without the owner's approval. Pin every dependency to an exact version.
|
|
37
|
+
Wait at least seven days after a version is released before adding it, and check that no newer
|
|
38
|
+
security release replaces it. Prefer language-official packages, then vendor packages, and avoid
|
|
39
|
+
single-maintainer packages.
|
|
40
|
+
|
|
41
|
+
archstrict analyzes with `typescript` pinned at `6.0.3`. CI (`.github/workflows/ci.yml`) runs the
|
|
42
|
+
main job on Node.js 22 and separately probes typescript 7 (`ci:ts7-probe`) without changing the
|
|
43
|
+
pinned dependency. The publish workflow (`.github/workflows/publish.yml`) requires Node.js 24.10
|
|
44
|
+
and npm 11.5.1 or newer before it runs `npm publish`. `package.json` currently declares no
|
|
45
|
+
`engines` field; adding one, and which Node.js versions it should name, is the owner's call.
|
|
46
|
+
|
|
47
|
+
## Add a rule or a verb
|
|
48
|
+
|
|
49
|
+
Preserve the violation contract when you add a rule:
|
|
50
|
+
|
|
51
|
+
- A violation carries a rule id, `path:line:col`, the evidence, a `because` reason, and a `do:`
|
|
52
|
+
command that runs, in both text and JSON output.
|
|
53
|
+
- A violation's text never uses the words "strict" or "typed" - a name must not mislead about what
|
|
54
|
+
fixes it.
|
|
55
|
+
- Text output stays bounded: past a fixed count, print grouped counts with one example per group
|
|
56
|
+
and a `do:` that reruns just that group. JSON output stays complete; only text output cuts.
|
|
57
|
+
- Point new prose at the config field that caused the violation, the same way an existing rule's
|
|
58
|
+
`because` reason does, so an agent can find the one line to change.
|
|
59
|
+
|
|
60
|
+
Preserve the same contract when you add a verb:
|
|
61
|
+
|
|
62
|
+
- Support both a text and a `--json` mode, with the same field names and order the existing verbs
|
|
63
|
+
use for the same concept (`do`, `error`, a per-item `path`).
|
|
64
|
+
- A config or missing-file failure throws so `main` in `src/cli.ts` catches it and prints
|
|
65
|
+
`{ "error": "<message>", "do": "<command>" }` (or the text equivalent) - do not print a partial
|
|
66
|
+
result and a stack trace instead.
|
|
67
|
+
- Add the verb to `AGENTS.md`'s Commands section and to the CLI usage line in `src/cli.ts`.
|
|
68
|
+
- Add example-based tests for exact text and JSON output, and Hegel property tests for any round
|
|
69
|
+
trip, invariant, or ordering rule the verb introduces.
|
|
70
|
+
|
|
71
|
+
## Triage a bug report
|
|
72
|
+
|
|
73
|
+
Ask for the project's `archstrict.config.ts`, the `archstrict check --json` output, and the
|
|
74
|
+
project's `archstrict.todo.json` if one exists. The config shows which module, tag, and edge rules
|
|
75
|
+
apply; the JSON output shows every violation with its rule id and evidence; the todo file shows
|
|
76
|
+
which of those are already frozen debt rather than new failures.
|
|
77
|
+
|
|
78
|
+
Reproduce the report by running `archstrict check` against a copy of the project, or against a
|
|
79
|
+
minimal fixture that keeps the same module and edge shape. Compare the text and `--json` output
|
|
80
|
+
against what the report describes. Add the fixture as a test case when it contains no sensitive
|
|
81
|
+
paths or names, then add an example test for the exact output and a property test for the broken
|
|
82
|
+
invariant, when one exists.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
archstrict stays on 0.x versions for now. A release has three parts: the npm package, a git tag,
|
|
4
|
+
and the skill and plugin.
|
|
5
|
+
|
|
6
|
+
Pushing a `v*` tag runs `.github/workflows/publish.yml`. The workflow checks out
|
|
7
|
+
the tagged commit, runs the release checks, and runs `npm publish`. npm authenticates with GitHub
|
|
8
|
+
Actions OIDC through the Trusted Publisher. The repository stores no `NPM_TOKEN`, and the workflow
|
|
9
|
+
uses no long-lived npm secret. The GitHub Environment `publish` is the human gate. Required
|
|
10
|
+
reviewers approve the job before it can publish.
|
|
11
|
+
|
|
12
|
+
Every `uses:` value in a workflow must use a full 40-hex commit SHA. Add the action version in a
|
|
13
|
+
trailing comment, for example `uses: actions/checkout@<40-hex> # vX.Y.Z`. A tag ref does not meet
|
|
14
|
+
this requirement.
|
|
15
|
+
|
|
16
|
+
## Trusted publisher
|
|
17
|
+
|
|
18
|
+
Create the npm Trusted Publisher with these case-sensitive fields:
|
|
19
|
+
|
|
20
|
+
- Organization or user: `meganemura`
|
|
21
|
+
- Repository: `archstrict`
|
|
22
|
+
- Workflow filename: `publish.yml`
|
|
23
|
+
- Environment name: `publish`
|
|
24
|
+
- Allowed action: `npm publish`
|
|
25
|
+
|
|
26
|
+
Create the GitHub Environment `publish` and add the required reviewers. A private repository
|
|
27
|
+
cannot hold an environment with required reviewers, so this step waits until the repository
|
|
28
|
+
becomes public. The deployment approval appears after a `v*` tag starts the workflow and the job
|
|
29
|
+
enters that environment.
|
|
30
|
+
|
|
31
|
+
The `repository.url` field in `package.json` points at this GitHub repository. npm checks that URL
|
|
32
|
+
against the workflow repository. After the first successful publish, require two-factor
|
|
33
|
+
authentication for the package and disallow token publishing. The Trusted Publisher will continue
|
|
34
|
+
to work.
|
|
35
|
+
|
|
36
|
+
## Each version
|
|
37
|
+
|
|
38
|
+
1. Replace `(unreleased)` in `CHANGELOG.md` with the release date. Set the same version in
|
|
39
|
+
`package.json` and `package-lock.json`.
|
|
40
|
+
2. Run `npm run build`, `npm run typecheck`, and `npm test`.
|
|
41
|
+
3. Run `npm pack --dry-run`. Read its file list. It must contain `dist/`, `docs/`, `skills/`, both
|
|
42
|
+
READMEs, the changelog, `AGENTS.md`, `llms.txt`, `.agents/`, and the license. It must not contain
|
|
43
|
+
`test/` or `.claude-team/`.
|
|
44
|
+
4. Commit the release as `chore: release 0.x.0`. Tag it as `v0.x.0`. The tag without `v` must equal
|
|
45
|
+
the `package.json` version. Push the commit and tag. The tag push starts the workflow.
|
|
46
|
+
5. Approve the `publish` environment for that Actions run. The workflow runs `npm ci`, the build,
|
|
47
|
+
typecheck, and tests before `npm publish`. `prepublishOnly` repeats those checks.
|
|
48
|
+
6. Extract only that version's notes. `--notes-file CHANGELOG.md` would include every version.
|
|
49
|
+
Use `awk '/^## 0.x.0/{in_version=1;next} /^## /{in_version=0} in_version' CHANGELOG.md > notes.md`.
|
|
50
|
+
Then run `gh release create v0.x.0 --title v0.x.0 --notes-file notes.md`.
|
|
51
|
+
7. Install or update the skill and plugin for the agents that use this repository, following
|
|
52
|
+
whatever install path Claude Code and the agent's own tooling document for a plugin repository
|
|
53
|
+
at that time; this repository names no fixed install command for that step.
|
|
54
|
+
|
|
55
|
+
The owner performs the commit, tag, push, environment approval, release creation, and npm publish.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Persistent graph cache
|
|
2
|
+
|
|
3
|
+
`check`, `check <file>`, `todo`, `rules`, `recommend`, `fix`'s own baseline, and `search` all build
|
|
4
|
+
their module graph through one cached path. It reads and writes
|
|
5
|
+
`node_modules/.cache/archstrict/edges.json` (a header) plus a fixed 64 shard files under
|
|
6
|
+
`node_modules/.cache/archstrict/edges/` in the analyzed project - a file's own shard is a stable
|
|
7
|
+
hash of its own project-relative path. `simulate` never reads or writes this cache; it keeps its
|
|
8
|
+
own in-memory overlay instead.
|
|
9
|
+
|
|
10
|
+
A build rewrites only the shards whose own files actually changed, never the whole cache: a touch
|
|
11
|
+
or a one-file edit rewrites one shard plus the header. A shard that is missing, unreadable, or
|
|
12
|
+
whose bytes no longer match the header's own recorded hash for it makes only that shard's own
|
|
13
|
+
files a cache miss (re-walked and re-resolved); it is never an error, and it never discards the
|
|
14
|
+
rest of the cache.
|
|
15
|
+
|
|
16
|
+
The cache stores one entry per analyzed file, keyed by its absolute path: that file's own
|
|
17
|
+
syntactic import list (never its AST), its resolved specifiers, and the compiler options and
|
|
18
|
+
package.json "type" it was parsed under. A file whose own mtime, size, effective compiler
|
|
19
|
+
options, and implied module format all still match is not reparsed; only a changed or new file is
|
|
20
|
+
reparsed, and only that one file.
|
|
21
|
+
|
|
22
|
+
Resolutions are reused outright while nothing that can affect a resolution answer has moved:
|
|
23
|
+
the analyzed file set, every package.json outside node_modules, the nearest lockfile, every
|
|
24
|
+
resolvable file outside node_modules (any extension a specifier could resolve to, whether
|
|
25
|
+
analyzed, excluded, or in dist/), every distinct effective compiler-options object, and the
|
|
26
|
+
node_modules package set on the path resolution actually walks (see below). The moment any of
|
|
27
|
+
these moves, every specifier is re-resolved project-wide from each file's own already-cached
|
|
28
|
+
import list - never a reparse of any file whose own mtime and size are unchanged.
|
|
29
|
+
|
|
30
|
+
The node_modules dependency set covers every node_modules directory this build meets: each
|
|
31
|
+
ancestor of the project root's own, all the way up to the filesystem root (the same distance
|
|
32
|
+
TypeScript's own resolver walks for a bare specifier, regardless of where a lockfile sits), and
|
|
33
|
+
every one the project's own tree walk meets while descending (a workspace member's own
|
|
34
|
+
node_modules, e.g. `packages/app/node_modules`), without ever descending into node_modules
|
|
35
|
+
itself. Each one records its own top-level package names and their package.json's own mtime,
|
|
36
|
+
read after following a symlink; a dot-prefixed entry (`.cache`, `.bin`, `.vite`, pnpm's own
|
|
37
|
+
`.pnpm` store) is never counted as a package name, so this cache's own
|
|
38
|
+
`node_modules/.cache/archstrict` does not move its own fingerprint. This covers a package
|
|
39
|
+
installed or removed with no lockfile edit, and a symlinked workspace package's own `exports`
|
|
40
|
+
edit. It does not cover an edit made directly to an already-installed package's own file, leaving
|
|
41
|
+
its package.json untouched - nothing this cache reads changes for that edit, and deleting
|
|
42
|
+
`node_modules/.cache/archstrict` is the only way to force a rebuild for it.
|
|
43
|
+
|
|
44
|
+
The reparse gate is mtime and size, not a content hash: an edit that keeps the exact same byte
|
|
45
|
+
size and whose mtime is restored (or never advances) is not detected either.
|
|
46
|
+
|
|
47
|
+
A package version mismatch, a change to this project's own built code (module-graph.js,
|
|
48
|
+
edge-cache.js), or a different installed typescript version drops the whole cache. A malformed
|
|
49
|
+
cache file is a silent miss, not an error; writes are atomic (a temporary file, then a rename),
|
|
50
|
+
and a failed write leaves the fresh analysis result usable regardless.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Migrating from the per-module todo layout
|
|
2
|
+
|
|
3
|
+
**Remove all of the following after the first release** - the migration
|
|
4
|
+
only matters for a project that adopted archstrict before the single-file
|
|
5
|
+
`archstrict.todo.json` existed:
|
|
6
|
+
|
|
7
|
+
- this file
|
|
8
|
+
- `src/todo-migration.ts` and `test/todo-migration.test.ts`, whole
|
|
9
|
+
- in `src/verbs/check.ts`: `readCurrentTodo`'s own `legacy`/`migrationNote`
|
|
10
|
+
branches (fold `readCurrentTodo` back down to reading only
|
|
11
|
+
`readTodoFile`), and the `migrationNote` plumbing into `result.notes`
|
|
12
|
+
- in `src/verbs/todo.ts`: `freezeOrPrune`'s own `legacy` branch and its
|
|
13
|
+
call to `deleteLegacyTodoFiles`
|
|
14
|
+
- in `src/todo-store.ts`'s `readTodoFile`: the `{ entries: [...] }`,
|
|
15
|
+
no-`schemaVersion` fallback that returns `undefined` instead of throwing
|
|
16
|
+
`malformed` - once nothing on the old layout exists to collide with the
|
|
17
|
+
new file's own path, an object without `schemaVersion` is just malformed
|
|
18
|
+
|
|
19
|
+
## The old layout
|
|
20
|
+
|
|
21
|
+
Before this change, frozen debt lived in one file per module:
|
|
22
|
+
`archstrict.todo.json` inside a directory module's own directory, or
|
|
23
|
+
`<file>.archstrict.todo.json` beside a single-file module (there is no
|
|
24
|
+
directory to hold a sibling file). A project-root marker file,
|
|
25
|
+
`.archstrict-todo-initialized`, recorded that `archstrict todo` had run at
|
|
26
|
+
all, since a project with zero freezable violations wrote no per-module
|
|
27
|
+
file on its first run and file-existence alone couldn't tell "first run,
|
|
28
|
+
genuinely clean" apart from "a later run."
|
|
29
|
+
|
|
30
|
+
On a 31-module project, that put 24 todo files scattered among the source
|
|
31
|
+
files, and a re-architecture created and removed a todo file with every
|
|
32
|
+
module it added or split.
|
|
33
|
+
|
|
34
|
+
## The new layout
|
|
35
|
+
|
|
36
|
+
One file at the project root, `archstrict.todo.json`, with a schema
|
|
37
|
+
version and entries grouped by module name. Its own existence now means
|
|
38
|
+
"the first run happened" - a project with no debt after its first run
|
|
39
|
+
still gets the file, with an empty module map, so the ratchet's own state
|
|
40
|
+
stays visible on disk. `.archstrict-todo-initialized` no longer exists.
|
|
41
|
+
|
|
42
|
+
## What migrates, and when
|
|
43
|
+
|
|
44
|
+
`archstrict todo` folds the old layout in automatically, the first time it
|
|
45
|
+
runs after this change ships: it reads every legacy per-module file and
|
|
46
|
+
the marker (if any), treats the result as a normal prune pass (never a
|
|
47
|
+
fresh first run, since the debt was already frozen under the old layout),
|
|
48
|
+
writes the new single file, then deletes every old file it read. A module
|
|
49
|
+
whose own glob covers the project root itself (`"**"`) has its legacy
|
|
50
|
+
per-module path collide with the new file's own path; that file is read,
|
|
51
|
+
then overwritten in place with the new, schema-versioned shape - never
|
|
52
|
+
queued for deletion, which would otherwise erase the migration in the same
|
|
53
|
+
run that produced it.
|
|
54
|
+
|
|
55
|
+
`archstrict check` reads the old layout too, until someone actually runs
|
|
56
|
+
`archstrict todo` - so an adopted project already covered by the old
|
|
57
|
+
layout keeps passing `check` across the upgrade, with a note in the report
|
|
58
|
+
pointing at `archstrict todo` to migrate.
|
package/llms.txt
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# archstrict
|
|
2
|
+
|
|
3
|
+
TypeScript module-boundary checker. A module is one directory, or one file, named in `archstrict.config.ts`. It shows the rest of the codebase one public-surface file; every other file inside it is private. Known violations freeze into one project-root `archstrict.todo.json`, grouped by module name, that can only shrink.
|
|
4
|
+
|
|
5
|
+
It is not a type checker. It does not compile TypeScript, format code, or lint style.
|
|
6
|
+
|
|
7
|
+
Agent skill: [skills/archstrict/SKILL.md](skills/archstrict/SKILL.md)
|
|
8
|
+
|
|
9
|
+
Installed package: `node_modules/archstrict/skills/archstrict/SKILL.md` (this file sits beside it at `node_modules/archstrict/llms.txt`).
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
- `archstrict init [dir] [--json]` — declare one module per top-level directory holding `.ts` and one per loose top-level `.ts` file, so the first `check` covers every file by construction; write `archstrict.types.ts` and, if missing, `archstrict.config.ts` (`schemaVersion: 1`)
|
|
14
|
+
- `archstrict check [file] [--json] [--rule <id>] [--module <name>] [--frozen]` — report violations for the whole project; a file argument only scopes the report. Up to 20 violations print in full; more print grouped counts with one example each, plus a `do:` per group. `--rule`/`--module` narrow the printed and JSON violations (repeatable); the exit code reflects the filtered set once a filter is given. `--frozen` also includes todo-matched violations, marked `frozen: true` and exempt from the exit code, so a re-architecting agent can read a module's frozen debt through the same filters instead of opening its todo JSON by hand
|
|
15
|
+
- `archstrict todo [--json]` — freeze current freezable violations on the first run, then only prune
|
|
16
|
+
- `archstrict recommend [dir] [--json]` — `patternProposals`: at most 5 detected boundary patterns (layered order, app over library, leaf/pure kernel, external package confined to one area, test code kept out of production, public-entry-only, host/plugin inversion, feature isolation around a shared kernel), ranked by how strongly the real edges support each, with evidence, a pasteable config fragment, and how many violations adopting it would add today; `surfaceProposals`: for each surface-less module, a ranked list of the files other modules actually import from it and the smallest ranked prefix covering at least 80% of those imports, offered as a `surface` value alongside adding a barrel file or freezing the module private
|
|
17
|
+
- `archstrict hotspots [--since <git ref or date>] [--json]` — combine Git change frequency and co-change with module fan-in, fan-out, frozen debt, and active violations
|
|
18
|
+
|
|
19
|
+
A config or missing-file failure prints a `do:` line naming the command to run. With `--json` that failure is `{ "error": "<message>", "do": "<command>" }`.
|
package/package.json
CHANGED
|
@@ -1,12 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archstrict",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript module boundary checker: default-private module surfaces, layers, cycles, and a todo freeze that can only shrink.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"architecture",
|
|
7
|
+
"boundaries",
|
|
8
|
+
"typescript",
|
|
9
|
+
"lint",
|
|
10
|
+
"module",
|
|
11
|
+
"agent",
|
|
12
|
+
"claude-code"
|
|
13
|
+
],
|
|
5
14
|
"license": "MIT",
|
|
6
15
|
"author": "meganemura",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/meganemura/archstrict.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/meganemura/archstrict#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/meganemura/archstrict/issues"
|
|
23
|
+
},
|
|
7
24
|
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"archstrict": "./dist/cli.js"
|
|
27
|
+
},
|
|
8
28
|
"files": [
|
|
9
29
|
"README.md",
|
|
10
|
-
"
|
|
11
|
-
|
|
30
|
+
"README.ja.md",
|
|
31
|
+
"CHANGELOG.md",
|
|
32
|
+
"AGENTS.md",
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"dist",
|
|
35
|
+
"docs",
|
|
36
|
+
"skills",
|
|
37
|
+
"llms.txt",
|
|
38
|
+
".agents"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:coverage": "vitest run --coverage --testTimeout=60000",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"build": "tsc -p tsconfig.build.json",
|
|
45
|
+
"dogfood:nukadoko": "npm run build && nuka run features",
|
|
46
|
+
"ci:ts7-probe": "node scripts/probe-typescript7.mjs",
|
|
47
|
+
"prepare": "npm run build",
|
|
48
|
+
"prepublishOnly": "npm run build && npm run typecheck && npm test"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
52
|
+
"typescript": "6.0.3"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@hegeldev/hegel": "0.4.5",
|
|
56
|
+
"@meganemura/depug": "0.1.3",
|
|
57
|
+
"@types/node": "26.5.1",
|
|
58
|
+
"@vitest/coverage-v8": "5.0.0",
|
|
59
|
+
"nukadoko": "0.12.0",
|
|
60
|
+
"vitest": "5.0.0"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=22"
|
|
64
|
+
}
|
|
12
65
|
}
|