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.
Files changed (65) hide show
  1. package/.agents/hooks/hooks.json +29 -0
  2. package/.agents/hooks/post-tool-use.mjs +107 -0
  3. package/.agents/hooks/pre-tool-use.mjs +182 -0
  4. package/.agents/mcp/server.mjs +71 -0
  5. package/.agents/plugin.json +19 -0
  6. package/AGENTS.md +69 -0
  7. package/CHANGELOG.md +38 -0
  8. package/README.ja.md +62 -0
  9. package/README.md +63 -2
  10. package/dist/augmentation-cache.js +65 -0
  11. package/dist/check-options.js +40 -0
  12. package/dist/classify.js +148 -0
  13. package/dist/cli.js +239 -0
  14. package/dist/config-pointer.js +251 -0
  15. package/dist/config.js +186 -0
  16. package/dist/edge-cache.js +530 -0
  17. package/dist/mcp-server.js +111 -0
  18. package/dist/module-candidates.js +118 -0
  19. package/dist/module-graph.js +2072 -0
  20. package/dist/project-path.js +59 -0
  21. package/dist/report-error.js +13 -0
  22. package/dist/rules/config-meaning.js +143 -0
  23. package/dist/rules/constraints.js +417 -0
  24. package/dist/rules/cycles.js +257 -0
  25. package/dist/rules/deprecated.js +67 -0
  26. package/dist/rules/empty-rule.js +101 -0
  27. package/dist/rules/moves.js +79 -0
  28. package/dist/rules/must-be-empty.js +52 -0
  29. package/dist/rules/public-surface.js +100 -0
  30. package/dist/rules/type-leak.js +562 -0
  31. package/dist/rules/uncovered.js +75 -0
  32. package/dist/todo-migration.js +112 -0
  33. package/dist/todo-store.js +434 -0
  34. package/dist/type-closure.js +959 -0
  35. package/dist/verbs/agents.js +116 -0
  36. package/dist/verbs/check.js +957 -0
  37. package/dist/verbs/fix.js +170 -0
  38. package/dist/verbs/hotspots.js +261 -0
  39. package/dist/verbs/init.js +522 -0
  40. package/dist/verbs/recommend.js +800 -0
  41. package/dist/verbs/rules.js +188 -0
  42. package/dist/verbs/search.js +109 -0
  43. package/dist/verbs/simulate.js +220 -0
  44. package/dist/verbs/todo.js +163 -0
  45. package/dist/warm-graph.js +82 -0
  46. package/docs/boundary-patterns.md +374 -0
  47. package/docs/calibrated-rules-design.md +124 -0
  48. package/docs/init-singleton-modules.md +128 -0
  49. package/docs/maintenance.md +82 -0
  50. package/docs/releasing.md +55 -0
  51. package/docs/rules-edge-cache.md +50 -0
  52. package/docs/todo-single-file-migration.md +58 -0
  53. package/llms.txt +19 -0
  54. package/package.json +57 -4
  55. package/skills/archstrict/SKILL.md +42 -0
  56. package/skills/archstrict/references/agents-verb.md +39 -0
  57. package/skills/archstrict/references/config.md +107 -0
  58. package/skills/archstrict/references/hook.md +57 -0
  59. package/skills/archstrict/references/path-rules.md +57 -0
  60. package/skills/archstrict/references/patterns.md +883 -0
  61. package/skills/archstrict/references/prove-rules.md +58 -0
  62. package/skills/archstrict/references/rearchitect.md +35 -0
  63. package/skills/archstrict/references/recommend.md +80 -0
  64. package/skills/archstrict/references/rules.md +146 -0
  65. 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.0.0",
4
- "description": "Reserved.",
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
- "LICENSE"
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
  }