@jamie-tam/forge 6.0.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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +389 -0
  3. package/agents/architect.md +92 -0
  4. package/agents/builder.md +122 -0
  5. package/agents/code-reviewer.md +107 -0
  6. package/agents/concept-designer.md +207 -0
  7. package/agents/craft-reviewer.md +132 -0
  8. package/agents/critic.md +130 -0
  9. package/agents/doc-writer.md +85 -0
  10. package/agents/dreamer.md +129 -0
  11. package/agents/e2e-runner.md +89 -0
  12. package/agents/gotcha-hunter.md +127 -0
  13. package/agents/prototype-builder.md +193 -0
  14. package/agents/prototype-codifier.md +204 -0
  15. package/agents/prototype-reviewer.md +163 -0
  16. package/agents/security-reviewer.md +108 -0
  17. package/agents/spec-reviewer.md +94 -0
  18. package/agents/tracer.md +98 -0
  19. package/agents/wireframer.md +109 -0
  20. package/commands/abort.md +25 -0
  21. package/commands/bugfix.md +151 -0
  22. package/commands/evolve.md +118 -0
  23. package/commands/feature.md +236 -0
  24. package/commands/forge.md +100 -0
  25. package/commands/greenfield.md +185 -0
  26. package/commands/hotfix.md +98 -0
  27. package/commands/refactor.md +147 -0
  28. package/commands/resume.md +25 -0
  29. package/commands/setup.md +201 -0
  30. package/commands/status.md +27 -0
  31. package/commands/task-force.md +110 -0
  32. package/commands/validate.md +12 -0
  33. package/dist/__tests__/active-manifest.test.js +272 -0
  34. package/dist/__tests__/copy.test.js +96 -0
  35. package/dist/__tests__/gate-check.test.js +384 -0
  36. package/dist/__tests__/wiki.test.js +472 -0
  37. package/dist/__tests__/work-manifest.test.js +304 -0
  38. package/dist/active-manifest.js +229 -0
  39. package/dist/cli.js +158 -0
  40. package/dist/copy.js +124 -0
  41. package/dist/gate-check.js +326 -0
  42. package/dist/hooks.js +60 -0
  43. package/dist/init.js +140 -0
  44. package/dist/manifest.js +90 -0
  45. package/dist/merge.js +77 -0
  46. package/dist/paths.js +36 -0
  47. package/dist/uninstall.js +216 -0
  48. package/dist/update.js +158 -0
  49. package/dist/verify-manifest.js +65 -0
  50. package/dist/verify.js +98 -0
  51. package/dist/wiki-ui.js +310 -0
  52. package/dist/wiki.js +364 -0
  53. package/dist/work-manifest.js +798 -0
  54. package/hooks/config/gate-requirements.json +79 -0
  55. package/hooks/hooks.json +143 -0
  56. package/hooks/scripts/analyze-telemetry.sh +114 -0
  57. package/hooks/scripts/gate-enforcer.sh +164 -0
  58. package/hooks/scripts/pre-compact.sh +90 -0
  59. package/hooks/scripts/session-start.sh +81 -0
  60. package/hooks/scripts/telemetry.sh +41 -0
  61. package/hooks/scripts/wiki-lint.sh +87 -0
  62. package/hooks/templates/AGENTS.md.template +48 -0
  63. package/hooks/templates/CLAUDE.md.template +45 -0
  64. package/package.json +55 -0
  65. package/protocols/README.md +40 -0
  66. package/protocols/codex.md +151 -0
  67. package/protocols/graphify.md +156 -0
  68. package/references/common/agent-coordination.md +65 -0
  69. package/references/common/coding-standards.md +54 -0
  70. package/references/common/feature-tracking.md +21 -0
  71. package/references/common/io-protocol.md +36 -0
  72. package/references/common/phases.md +57 -0
  73. package/references/common/quality-gates.md +130 -0
  74. package/references/common/skill-authoring.md +154 -0
  75. package/references/common/skill-compliance.md +30 -0
  76. package/references/python/standards.md +44 -0
  77. package/references/react/standards.md +61 -0
  78. package/references/typescript/standards.md +42 -0
  79. package/rules/common/forge-system.md +59 -0
  80. package/rules/common/git-workflow.md +40 -0
  81. package/rules/common/guardrails.md +37 -0
  82. package/rules/common/quality-gates.md +18 -0
  83. package/rules/common/security.md +50 -0
  84. package/rules/common/skill-selection.md +78 -0
  85. package/rules/common/testing.md +58 -0
  86. package/rules/common/verification.md +39 -0
  87. package/skills/build-pr-workflow/SKILL.md +301 -0
  88. package/skills/build-pr-workflow/references/pr-template.md +62 -0
  89. package/skills/build-pr-workflow/references/subagent-merge.md +47 -0
  90. package/skills/build-pr-workflow/references/worktree-setup.md +125 -0
  91. package/skills/build-prototype/SKILL.md +264 -0
  92. package/skills/build-scaffold/SKILL.md +340 -0
  93. package/skills/build-tdd/SKILL.md +89 -0
  94. package/skills/build-wireframe/SKILL.md +110 -0
  95. package/skills/build-wireframe/assets/baseline-template.html +486 -0
  96. package/skills/build-wireframe/references/demo-walkthroughs.md +170 -0
  97. package/skills/build-wireframe/references/gotchas.md +188 -0
  98. package/skills/build-wireframe/references/legend-lines.md +141 -0
  99. package/skills/concept-slides/SKILL.md +192 -0
  100. package/skills/deliver-db-migration/SKILL.md +466 -0
  101. package/skills/deliver-deploy/SKILL.md +407 -0
  102. package/skills/deliver-onboarding/SKILL.md +198 -0
  103. package/skills/deliver-onboarding/references/document-templates.md +393 -0
  104. package/skills/deliver-onboarding/templates/getting-started.md +122 -0
  105. package/skills/discover-codebase-analysis/SKILL.md +448 -0
  106. package/skills/discover-requirements/SKILL.md +418 -0
  107. package/skills/discover-requirements/templates/prd.md +99 -0
  108. package/skills/discover-requirements/templates/technical-spec.md +123 -0
  109. package/skills/discover-requirements/templates/user-stories.md +76 -0
  110. package/skills/harden/SKILL.md +214 -0
  111. package/skills/iterate-prototype/SKILL.md +241 -0
  112. package/skills/plan-architecture/SKILL.md +457 -0
  113. package/skills/plan-architecture/templates/adr-template.md +52 -0
  114. package/skills/plan-architecture/templates/api-contract.md +99 -0
  115. package/skills/plan-architecture/templates/db-schema.md +81 -0
  116. package/skills/plan-architecture/templates/system-design.md +111 -0
  117. package/skills/plan-brainstorm/SKILL.md +433 -0
  118. package/skills/plan-design-system/SKILL.md +279 -0
  119. package/skills/plan-task-decompose/SKILL.md +454 -0
  120. package/skills/quality-code-review/SKILL.md +286 -0
  121. package/skills/quality-security-audit/SKILL.md +292 -0
  122. package/skills/quality-security-audit/references/audit-report-template.md +89 -0
  123. package/skills/quality-security-audit/references/owasp-checks.md +178 -0
  124. package/skills/quality-test-execution/SKILL.md +435 -0
  125. package/skills/quality-test-plan/SKILL.md +297 -0
  126. package/skills/quality-test-plan/references/test-type-guide.md +263 -0
  127. package/skills/quality-test-plan/templates/e2e-test-plan.md +72 -0
  128. package/skills/quality-test-plan/templates/integration-test-plan.md +74 -0
  129. package/skills/quality-test-plan/templates/load-test-plan.md +111 -0
  130. package/skills/quality-test-plan/templates/smoke-test-plan.md +68 -0
  131. package/skills/quality-test-plan/templates/unit-test-plan.md +56 -0
  132. package/skills/quality-uiux/SKILL.md +481 -0
  133. package/skills/support-debug/SKILL.md +464 -0
  134. package/skills/support-dream/SKILL.md +213 -0
  135. package/skills/support-gotcha/SKILL.md +249 -0
  136. package/skills/support-runtime-reachability/SKILL.md +190 -0
  137. package/skills/support-runtime-reachability/scripts/__fixtures__/case-01-passes-app-use/src/app.ts +7 -0
  138. package/skills/support-runtime-reachability/scripts/__fixtures__/case-01-passes-app-use/src/handlers/cases.ts +7 -0
  139. package/skills/support-runtime-reachability/scripts/__fixtures__/case-02-orphan-no-app-use/src/app.ts +8 -0
  140. package/skills/support-runtime-reachability/scripts/__fixtures__/case-02-orphan-no-app-use/src/handlers/cases.ts +7 -0
  141. package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/App.tsx +5 -0
  142. package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/components/RingingBanner.tsx +7 -0
  143. package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/hooks/useTwilio.ts +6 -0
  144. package/skills/support-runtime-reachability/scripts/__fixtures__/case-04-jsx-component-rendered/src/App.tsx +5 -0
  145. package/skills/support-runtime-reachability/scripts/__fixtures__/case-04-jsx-component-rendered/src/components/MyComp.tsx +3 -0
  146. package/skills/support-runtime-reachability/scripts/__fixtures__/case-05-jsx-component-not-rendered/src/App.tsx +3 -0
  147. package/skills/support-runtime-reachability/scripts/__fixtures__/case-05-jsx-component-not-rendered/src/components/Orphan.tsx +3 -0
  148. package/skills/support-runtime-reachability/scripts/__fixtures__/case-06-class-instantiated/src/lib/Service.ts +6 -0
  149. package/skills/support-runtime-reachability/scripts/__fixtures__/case-06-class-instantiated/src/main.ts +4 -0
  150. package/skills/support-runtime-reachability/scripts/__fixtures__/case-07-class-not-instantiated/src/lib/Lonely.ts +5 -0
  151. package/skills/support-runtime-reachability/scripts/__fixtures__/case-07-class-not-instantiated/src/main.ts +2 -0
  152. package/skills/support-runtime-reachability/scripts/__fixtures__/case-08-default-export-imported-and-called/src/handler.ts +3 -0
  153. package/skills/support-runtime-reachability/scripts/__fixtures__/case-08-default-export-imported-and-called/src/main.ts +3 -0
  154. package/skills/support-runtime-reachability/scripts/__fixtures__/case-09-default-export-orphan/src/handler.ts +3 -0
  155. package/skills/support-runtime-reachability/scripts/__fixtures__/case-09-default-export-orphan/src/main.ts +2 -0
  156. package/skills/support-runtime-reachability/scripts/__fixtures__/case-10-aliased-named-export/src/lib.ts +5 -0
  157. package/skills/support-runtime-reachability/scripts/__fixtures__/case-10-aliased-named-export/src/main.ts +3 -0
  158. package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/lib/index.ts +1 -0
  159. package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/lib/internal.ts +3 -0
  160. package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/main.ts +3 -0
  161. package/skills/support-runtime-reachability/scripts/__fixtures__/case-12-test-only-caller/src/util.test.ts +5 -0
  162. package/skills/support-runtime-reachability/scripts/__fixtures__/case-12-test-only-caller/src/util.ts +3 -0
  163. package/skills/support-runtime-reachability/scripts/__fixtures__/case-13-gated-pending-annotation/src/future.ts +4 -0
  164. package/skills/support-runtime-reachability/scripts/__fixtures__/case-14-untraceable-annotation/src/decorated.ts +4 -0
  165. package/skills/support-runtime-reachability/scripts/__fixtures__/case-15-untraceable-empty/src/lazy.ts +4 -0
  166. package/skills/support-runtime-reachability/scripts/__fixtures__/case-16-python-module/src/lib.py +15 -0
  167. package/skills/support-runtime-reachability/scripts/__fixtures__/case-16-python-module/src/main.py +5 -0
  168. package/skills/support-runtime-reachability/scripts/__fixtures__/case-17-router-use/src/parent.ts +5 -0
  169. package/skills/support-runtime-reachability/scripts/__fixtures__/case-17-router-use/src/routes/cases.ts +5 -0
  170. package/skills/support-runtime-reachability/scripts/__fixtures__/case-18-shadowed-name-fp/src/lib/foo.ts +3 -0
  171. package/skills/support-runtime-reachability/scripts/__fixtures__/case-18-shadowed-name-fp/src/other.ts +8 -0
  172. package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/handlers/cases.ts +4 -0
  173. package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/handlers/users.ts +4 -0
  174. package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/main.ts +5 -0
  175. package/skills/support-runtime-reachability/scripts/__fixtures__/case-20-aliased-import-usage/src/handlers/cases.ts +3 -0
  176. package/skills/support-runtime-reachability/scripts/__fixtures__/case-20-aliased-import-usage/src/main.ts +4 -0
  177. package/skills/support-runtime-reachability/scripts/__fixtures__/case-21-mixed-default-and-named/src/lib.ts +5 -0
  178. package/skills/support-runtime-reachability/scripts/__fixtures__/case-21-mixed-default-and-named/src/main.ts +5 -0
  179. package/skills/support-runtime-reachability/scripts/__fixtures__/case-22-dynamic-import-then-caller/src/lib.ts +3 -0
  180. package/skills/support-runtime-reachability/scripts/__fixtures__/case-22-dynamic-import-then-caller/src/main.ts +8 -0
  181. package/skills/support-runtime-reachability/scripts/__fixtures__/case-23-dynamic-import-with-space/src/lib.ts +3 -0
  182. package/skills/support-runtime-reachability/scripts/__fixtures__/case-23-dynamic-import-with-space/src/main.ts +7 -0
  183. package/skills/support-runtime-reachability/scripts/check.mjs +638 -0
  184. package/skills/support-runtime-reachability/scripts/check.test.mjs +244 -0
  185. package/skills/support-skill-validator/SKILL.md +194 -0
  186. package/skills/support-skill-validator/references/false-positives.md +59 -0
  187. package/skills/support-skill-validator/references/validation-checks.md +280 -0
  188. package/skills/support-system-guide/SKILL.md +311 -0
  189. package/skills/support-task-force/SKILL.md +265 -0
  190. package/skills/support-task-force/references/dispatch-pattern.md +178 -0
  191. package/skills/support-task-force/references/synthesis-template.md +126 -0
  192. package/skills/support-wiki-bootstrap/SKILL.md +37 -0
  193. package/skills/support-wiki-lint/SKILL.md +196 -0
  194. package/skills/support-wiki-lint/scripts/lint.mjs +488 -0
  195. package/skills/support-wiki-lint/scripts/lint.test.mjs +196 -0
  196. package/templates/README.md +23 -0
  197. package/templates/aiwiki/CLAUDE.md.template +78 -0
  198. package/templates/aiwiki/schemas/architecture.md +118 -0
  199. package/templates/aiwiki/schemas/convention.md +112 -0
  200. package/templates/aiwiki/schemas/decision.md +144 -0
  201. package/templates/aiwiki/schemas/gotcha.md +118 -0
  202. package/templates/aiwiki/schemas/oracle.md +105 -0
  203. package/templates/aiwiki/schemas/session.md +125 -0
  204. package/templates/manifests/bugfix.yaml +41 -0
  205. package/templates/manifests/feature.yaml +69 -0
  206. package/templates/manifests/greenfield.yaml +61 -0
  207. package/templates/manifests/hotfix.yaml +45 -0
  208. package/templates/manifests/refactor.yaml +44 -0
  209. package/templates/manifests/v5/SCHEMA.md +327 -0
  210. package/templates/manifests/v5/feature.yaml +77 -0
  211. package/templates/manifests/v6/SCHEMA.md +199 -0
  212. package/templates/wiki-html/dream-detail.html +378 -0
  213. package/templates/wiki-html/dreams-list.html +155 -0
@@ -0,0 +1,196 @@
1
+ ---
2
+ name: support-wiki-lint
3
+ description: "Use to validate a wiki page against its schema — required frontmatter, required H2 sections, line caps, citation hash drift. Runs synchronously on every aiwiki/** write via PostToolUse hook, and on dream output before user review. Catches schema violations and stale citations the moment they happen."
4
+ ---
5
+
6
+ # Support: Wiki Lint
7
+
8
+ ## Overview
9
+
10
+ The wiki is curated to answer recurring questions. That only works if every page conforms to its declared schema (so AI can find what it needs predictably) and every code citation still points at the code that exists today (so claims don't go silently stale). This skill enforces both — fast, deterministic, blocking.
11
+
12
+ **Core principle:** validation is mechanical. No judgment, no "this looks roughly right." Either the page matches its schema and citations resolve, or it doesn't.
13
+
14
+ **Announce at start:** "I'm using the support-wiki-lint skill to validate the wiki page."
15
+
16
+ ## When to Use
17
+
18
+ - Synchronously on every `aiwiki/**` write (PostToolUse hook fires the script)
19
+ - On dream output in `aiwiki/proposed/{dream_id}/` before the dream is marked complete
20
+ - On-demand when validating a wiki page manually (e.g. after a refactor that may have moved cited code)
21
+
22
+ **Do NOT skip when:**
23
+ - The page "looks fine" — schema drift accumulates silently; that's why we lint
24
+ - The change is "just a typo" — a typo in frontmatter (`schema_id: decsion`) breaks downstream tooling
25
+ - The dream just produced the file — dreamer's output goes through lint like every other write
26
+
27
+ ## Scope
28
+
29
+ Catches four classes of wiki defect:
30
+
31
+ | Defect | Example | Severity |
32
+ |---|---|---|
33
+ | Missing or malformed frontmatter | No `schema_id`; `schema_version` is a string instead of integer | error (block) |
34
+ | Missing required H2 section | ADR with no `## Decision` | error (block) |
35
+ | Section order violation when schema declares `section_order: strict` | `## Decision` before `## Context` | error (block) |
36
+ | Line cap exceeded | ADR > 400 lines | error (block) |
37
+ | Stale citation hash | `file:line@a3f2bc1` but recomputed hash is now `b1c2d3e` | error (block) |
38
+ | Missing citation hash on a `file:line` reference | `src/auth.ts:42` instead of `src/auth.ts:42@a3f2bc1` | warning (auto-backfill) |
39
+ | Cited file does not exist | `src/old.ts:42@a3f2bc1` after `src/old.ts` was deleted | error (block) |
40
+ | Soft-target line count exceeded | Gotcha at 110 lines (soft target 50-100) | warning (don't block) |
41
+
42
+ Out of scope (intentionally — these are someone else's job):
43
+ - Semantic content quality ("is this ADR's rationale sound?") → reviewer subagents
44
+ - Cross-page consistency ("does this ADR contradict another?") → dream consolidation
45
+ - Markdown rendering correctness → markdown linter (different tool)
46
+
47
+ ## I/O Contract
48
+
49
+ | Field | Value |
50
+ |---|---|
51
+ | **Requires** | Target file path, repo root, schema directory (`aiwiki/schemas/`) |
52
+ | **Produces** | JSON to stdout: `{ok: bool, errors: [...], warnings: [...], updates: [...] }`. Exit 0 on pass, exit 1 on lint failure, exit 2 on internal error. |
53
+ | **Side effect** | Auto-backfills missing `@<sha7>` citation hashes in-place (via `updates[]`). Auto-backfill is the ONLY in-place modification; everything else is reported, not fixed. |
54
+ | **Feeds into** | PostToolUse hook (block on exit ≠ 0); dream-completion gate; manual `/wiki lint` invocation |
55
+
56
+ ## Process
57
+
58
+ ### Step 0: locate inputs
59
+
60
+ | Input | How to find |
61
+ |---|---|
62
+ | Repo root | `git rev-parse --show-toplevel`, or `${CLAUDE_PROJECT_DIR}` if set |
63
+ | Schema directory | `<repo-root>/aiwiki/schemas/`. If missing, the wiki isn't initialized — surface the gap and stop |
64
+ | Target file path | Provided by caller (hook passes the file being written; manual invocation passes user-specified path) |
65
+ | Script path | `<repo-root>/.claude/skills/support-wiki-lint/scripts/lint.mjs`. If missing, the skill isn't installed in this project — surface the gap |
66
+
67
+ ### Step 1: invoke the lint script
68
+
69
+ ```bash
70
+ node "<repo-root>/.claude/skills/support-wiki-lint/scripts/lint.mjs" \
71
+ --file "<target-file-path>" \
72
+ --schemas "<repo-root>/aiwiki/schemas/" \
73
+ --root "<repo-root>"
74
+ ```
75
+
76
+ Output: JSON to stdout. Exit codes: 0 = pass, 1 = lint failure, 2 = internal error.
77
+
78
+ Output shape:
79
+ ```json
80
+ {
81
+ "ok": false,
82
+ "file": "aiwiki/decisions/0042-token-storage.md",
83
+ "schema_id": "decision",
84
+ "errors": [
85
+ {"kind": "missing_section", "section": "## Review", "message": "Required section '## Review' not found"},
86
+ {"kind": "stale_citation", "citation": "src/auth.ts:42@a3f2bc1", "message": "Hash mismatch: recomputed b1c2d3e", "expected": "a3f2bc1", "actual": "b1c2d3e"}
87
+ ],
88
+ "warnings": [
89
+ {"kind": "soft_cap_exceeded", "lines": 247, "soft_target": [100, 200], "message": "Page is 247 lines; soft target is 100-200"}
90
+ ],
91
+ "updates": [
92
+ {"kind": "citation_hash_backfilled", "before": "src/auth.ts:88", "after": "src/auth.ts:88@e5f6789"}
93
+ ]
94
+ }
95
+ ```
96
+
97
+ ### Step 2: interpret and report
98
+
99
+ If `ok: true`: one line — `wiki-lint: <file> ok` (with note if `updates[]` non-empty).
100
+
101
+ If `ok: false`: structured report grouped by error kind. Example:
102
+
103
+ ```markdown
104
+ ## wiki-lint: FAILED — aiwiki/decisions/0042-token-storage.md
105
+
106
+ ### Schema violations (2)
107
+ 1. Missing required section `## Review` (decision schema requires it for status: accepted)
108
+ 2. Section order: `## Decision` appears before `## Context` (schema declares strict order)
109
+
110
+ ### Stale citations (1)
111
+ 1. `src/auth.ts:42@a3f2bc1` — hash recomputed as `b1c2d3e`. Either:
112
+ - Update the citation to the new line / hash
113
+ - Remove the claim that depended on it
114
+ - Annotate the citation line with `// ack-stale: <reason>` if the staleness is acceptable for now
115
+ ```
116
+
117
+ The user fixes; re-run.
118
+
119
+ ### Step 3: handle auto-backfills
120
+
121
+ When the script reports `updates[]`, the citation hashes have already been written in-place. Surface them as informational:
122
+
123
+ ```
124
+ wiki-lint: 2 citation hash(es) auto-backfilled.
125
+ - src/auth.ts:88 → src/auth.ts:88@e5f6789
126
+ - src/cache.ts:14 → src/cache.ts:14@b1c2d3e
127
+ ```
128
+
129
+ Do not require user action for backfills — they're a courtesy, not a finding.
130
+
131
+ ## Schema validation rules
132
+
133
+ The script reads `aiwiki/schemas/{schema_id}.md` and applies its declared rules:
134
+
135
+ - `required_frontmatter` — every key must be present in target's frontmatter. Type-check (`type: integer`, `type: enum`, `type: date`) where declared. Optional fields (`optional: true`) may be absent or null.
136
+ - `required_sections` — every section name must appear as a `## <name>` line in target.
137
+ - `section_order: strict` — required sections must appear in the same order as declared.
138
+ - `section_order: flexible` — required sections must all appear, but in any order.
139
+ - `hard_cap_lines` — total file line count must not exceed.
140
+ - `soft_target_lines: [min, max]` — total line count outside this range = warning, not error.
141
+ - `citation_rule: required` — every code claim must have a `file:line@<sha7>` or `symbol` citation.
142
+ - `citation_rule: required-in-<section>` — only the named section requires citations.
143
+ - `citation_rule: required-where-claims-about-code` — heuristic mode (script only flags missing citations on lines that mention a function/class/file name without a citation).
144
+
145
+ Schemas live at `aiwiki/schemas/{schema_id}.md`. If the target's `schema_id` doesn't resolve to a schema file, that's a `missing_schema` error.
146
+
147
+ ## Citation rules
148
+
149
+ Two citation forms:
150
+
151
+ - `file:line@<sha7>` — e.g. `src/auth.ts:42@a3f2bc1`. The `@<sha7>` is the first 7 chars of `sha256(content)`, where `content` is the cited line ±2 lines (5 lines total) joined with `\n` (LF). Padded with empty lines if the cited line is within 2 of file start/end.
152
+ - `symbol` — e.g. `src/auth.ts#login`. Matches the symbol name; no hash. Less precise but tolerant of line drift.
153
+
154
+ **Auto-backfill**: a `file:line` reference without `@<sha7>` is backfilled in-place on first lint. Same for hashless symbol citations (no hash to add — verified existence only).
155
+
156
+ **Staleness**: a `@<sha7>` that doesn't match the recomputed hash fails lint. Resolve by:
157
+ 1. Updating the citation to point at the new location
158
+ 2. Removing the claim that depended on it
159
+ 3. Annotating with `// ack-stale: <reason>` on the citation line — the script accepts this as a deliberate mark and warns instead of failing
160
+
161
+ ## Common errors
162
+
163
+ | Error | Cause | Fix |
164
+ |---|---|---|
165
+ | `missing_schema` | Target's `schema_id` doesn't have a corresponding `aiwiki/schemas/{id}.md` | Either typo'd `schema_id`, or the schema wasn't initialized — check `aiwiki/schemas/` |
166
+ | `frontmatter_invalid` | YAML parse failed, or required field missing | Compare target's frontmatter against the schema's `required_frontmatter` block |
167
+ | `missing_section` | Required H2 not present | Add the section (see schema's "Required sections" body for purpose); if you genuinely don't have content for it, the page may not match this schema type |
168
+ | `section_order_violation` | Schema declares `section_order: strict`, target's sections are reordered | Reorder to match the schema |
169
+ | `hard_cap_exceeded` | File too long | Split into multiple files (architecture/), or trim to fit (gotcha/convention) |
170
+ | `stale_citation` | Cited code moved or content changed | Update citation, remove claim, or annotate `// ack-stale: <reason>` |
171
+ | `missing_file_in_citation` | Cited file path no longer exists | Update or remove the claim |
172
+
173
+ ## Red Flags
174
+
175
+ **Never:**
176
+ - Suppress lint findings to make a page pass — fix the page or fix the schema
177
+ - Annotate every stale citation as `ack-stale` to silence the gate (high `ack-stale` density signals architectural drift, not lint problems)
178
+ - Modify the page outside the auto-backfill (citation hashes) — every other change is the user's
179
+
180
+ **Always:**
181
+ - Run lint after every wiki write (the hook does this; manual edits via the agent should still trigger validation)
182
+ - Report all findings — never filter for brevity
183
+ - Re-run after fixing — drift in one section can mask drift in another
184
+
185
+ ## Integration
186
+
187
+ | Caller | When |
188
+ |---|---|
189
+ | `PostToolUse` hook on `aiwiki/**` | Synchronous validation on every wiki write |
190
+ | `support-dream` skill | After dream produces `aiwiki/proposed/{dream_id}/`, before marking the dream complete |
191
+ | Manual `/wiki lint <file>` | User-invoked validation |
192
+
193
+ | Pairs with | For |
194
+ |---|---|
195
+ | `support-dream` | Dream output passes through this lint before user review |
196
+ | Wiki schemas in `aiwiki/schemas/` | The validation rules live there; this skill enforces them |
@@ -0,0 +1,488 @@
1
+ #!/usr/bin/env node
2
+ // Wiki lint (v6.0).
3
+ // Validates a wiki page against its declared schema: required frontmatter,
4
+ // required H2 sections, section order, line caps, and citation hash freshness.
5
+ // Auto-backfills missing @<sha7> citation hashes in-place. Outputs JSON to
6
+ // stdout. Exit 0 on pass, 1 on lint failure, 2 on internal error.
7
+ //
8
+ // Hermetic: depends only on node: built-in modules. Uses an inline minimal
9
+ // YAML parser (handles the subset our schemas + frontmatter use; not a
10
+ // general-purpose YAML library).
11
+ //
12
+ // Usage:
13
+ // node lint.mjs --file <path> --schemas <dir> --root <dir>
14
+
15
+ import * as fs from 'node:fs';
16
+ import * as path from 'node:path';
17
+ import * as crypto from 'node:crypto';
18
+ import { parseArgs } from 'node:util';
19
+
20
+ // ---- Constants -------------------------------------------------------------
21
+
22
+ const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
23
+ const CITATION_HASHED_RE = /([\w./@~-]+\.\w+):(\d+)@([0-9a-f]{7})\b/g;
24
+ const CITATION_BARE_RE = /([\w./@~-]+\.\w+):(\d+)(?!@?[0-9a-f]{7})\b/g;
25
+ const ACK_STALE_RE = /\/\/\s*ack-stale\s*:|#\s*ack-stale\s*:/;
26
+ const SECTION_RE = /^##\s+(.+?)\s*$/gm;
27
+
28
+ // ---- Minimal YAML parser ---------------------------------------------------
29
+ // Handles only what our schema + frontmatter files use:
30
+ // - Top-level mapping
31
+ // - Scalars: string (bare or quoted), integer, float, boolean, null (~ or null)
32
+ // - Block sequences: `key:\n - item\n - item`
33
+ // - Block mappings: `key:\n subkey: subvalue`
34
+ // - Inline mappings: `key: { sub: val, sub: val }`
35
+ // - Inline sequences: `key: [val, val]`
36
+ // - Comments: `#` to end of line (line-leading or after value)
37
+ // Does NOT handle: anchors, aliases, multi-doc, complex keys, folded scalars.
38
+
39
+ export function parseYaml(text) {
40
+ const rawLines = text.split('\n');
41
+ const lines = rawLines.map((line) => {
42
+ // Strip line comments — but only when # is preceded by whitespace
43
+ // or starts the line; not inside quoted strings or URLs (rare in our schemas).
44
+ const m = line.match(/^([^#]*?)(?:\s+#.*)?$/);
45
+ return m ? m[1].replace(/\s+$/, '') : line;
46
+ }).filter((line, i) => {
47
+ const orig = rawLines[i];
48
+ const t = orig.trim();
49
+ return t !== '' && !t.startsWith('#');
50
+ });
51
+ return parseBlock(lines, 0).value;
52
+ }
53
+
54
+ function indentOf(line) {
55
+ const m = line.match(/^( *)/);
56
+ return m ? m[1].length : 0;
57
+ }
58
+
59
+ function parseBlock(lines, startIdx, baseIndent = 0) {
60
+ if (startIdx >= lines.length) return { value: {}, next: startIdx };
61
+ const firstLine = lines[startIdx];
62
+ const firstIndent = indentOf(firstLine);
63
+
64
+ // Decide if block is sequence or mapping based on first line
65
+ if (firstLine.trim().startsWith('- ')) {
66
+ return parseBlockSequence(lines, startIdx, firstIndent);
67
+ }
68
+ return parseBlockMapping(lines, startIdx, firstIndent);
69
+ }
70
+
71
+ function parseBlockMapping(lines, startIdx, indent) {
72
+ const result = {};
73
+ let i = startIdx;
74
+ while (i < lines.length) {
75
+ const line = lines[i];
76
+ const lineIndent = indentOf(line);
77
+ if (lineIndent < indent) break;
78
+ if (lineIndent > indent) {
79
+ // Skip — should have been consumed by recursive call
80
+ i++;
81
+ continue;
82
+ }
83
+ const trimmed = line.trim();
84
+ const m = trimmed.match(/^([\w\-]+)\s*:\s*(.*)$/);
85
+ if (!m) {
86
+ i++;
87
+ continue;
88
+ }
89
+ const key = m[1];
90
+ const rest = m[2];
91
+
92
+ if (rest === '' || rest === '~' || rest === 'null') {
93
+ // Block-style: peek next line for child indent
94
+ const nextIdx = i + 1;
95
+ if (nextIdx >= lines.length || indentOf(lines[nextIdx]) <= indent) {
96
+ result[key] = null;
97
+ i = nextIdx;
98
+ continue;
99
+ }
100
+ const childIndent = indentOf(lines[nextIdx]);
101
+ const child = parseBlock(lines, nextIdx, childIndent);
102
+ result[key] = child.value;
103
+ i = child.next;
104
+ } else {
105
+ result[key] = parseInlineValue(rest);
106
+ i++;
107
+ }
108
+ }
109
+ return { value: result, next: i };
110
+ }
111
+
112
+ function parseBlockSequence(lines, startIdx, indent) {
113
+ const items = [];
114
+ let i = startIdx;
115
+ while (i < lines.length) {
116
+ const line = lines[i];
117
+ const lineIndent = indentOf(line);
118
+ if (lineIndent !== indent) break;
119
+ const trimmed = line.trim();
120
+ if (!trimmed.startsWith('- ')) break;
121
+ items.push(parseInlineValue(trimmed.slice(2).trim()));
122
+ i++;
123
+ }
124
+ return { value: items, next: i };
125
+ }
126
+
127
+ function parseInlineValue(s) {
128
+ s = s.trim();
129
+ if (s === '' || s === '~' || s === 'null') return null;
130
+ if (s === 'true') return true;
131
+ if (s === 'false') return false;
132
+ if (s.startsWith('{') && s.endsWith('}')) return parseInlineMapping(s);
133
+ if (s.startsWith('[') && s.endsWith(']')) return parseInlineArray(s);
134
+ if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
135
+ return s.slice(1, -1);
136
+ }
137
+ if (/^-?\d+$/.test(s)) return parseInt(s, 10);
138
+ if (/^-?\d+\.\d+$/.test(s)) return parseFloat(s);
139
+ // ISO date — keep as string (consumers compare by string)
140
+ return s;
141
+ }
142
+
143
+ function parseInlineMapping(s) {
144
+ const inner = s.slice(1, -1).trim();
145
+ if (!inner) return {};
146
+ const result = {};
147
+ for (const part of splitTopLevel(inner, ',')) {
148
+ const m = part.trim().match(/^([\w\-]+)\s*:\s*(.*)$/);
149
+ if (m) {
150
+ result[m[1]] = parseInlineValue(m[2]);
151
+ }
152
+ }
153
+ return result;
154
+ }
155
+
156
+ function parseInlineArray(s) {
157
+ const inner = s.slice(1, -1).trim();
158
+ if (!inner) return [];
159
+ return splitTopLevel(inner, ',').map((p) => parseInlineValue(p.trim()));
160
+ }
161
+
162
+ function splitTopLevel(s, sep) {
163
+ const out = [];
164
+ let depth = 0;
165
+ let inSingle = false;
166
+ let inDouble = false;
167
+ let start = 0;
168
+ for (let i = 0; i < s.length; i++) {
169
+ const c = s[i];
170
+ if (!inSingle && !inDouble) {
171
+ if (c === '{' || c === '[') depth++;
172
+ else if (c === '}' || c === ']') depth--;
173
+ else if (c === '"') inDouble = true;
174
+ else if (c === "'") inSingle = true;
175
+ else if (c === sep && depth === 0) {
176
+ out.push(s.slice(start, i));
177
+ start = i + 1;
178
+ }
179
+ } else if (inDouble && c === '"') inDouble = false;
180
+ else if (inSingle && c === "'") inSingle = false;
181
+ }
182
+ out.push(s.slice(start));
183
+ return out;
184
+ }
185
+
186
+ // ---- Public API ------------------------------------------------------------
187
+
188
+ export function lint({ file, schemasDir, root }) {
189
+ const errors = [];
190
+ const warnings = [];
191
+ const updates = [];
192
+
193
+ const absFile = path.resolve(root, file);
194
+ if (!fs.existsSync(absFile)) {
195
+ return { ok: false, file, errors: [{ kind: 'file_not_found', message: `File not found: ${file}` }], warnings, updates };
196
+ }
197
+ const original = fs.readFileSync(absFile, 'utf8');
198
+
199
+ const fmMatch = FRONTMATTER_RE.exec(original);
200
+ if (!fmMatch) {
201
+ return { ok: false, file, errors: [{ kind: 'frontmatter_invalid', message: 'No YAML frontmatter block found (expected `---\\n...\\n---\\n` at file start)' }], warnings, updates };
202
+ }
203
+ const [, fmText, body] = fmMatch;
204
+
205
+ let fm;
206
+ try {
207
+ fm = parseYaml(fmText);
208
+ } catch (err) {
209
+ return { ok: false, file, errors: [{ kind: 'frontmatter_invalid', message: `YAML parse error: ${err.message}` }], warnings, updates };
210
+ }
211
+ if (!fm || typeof fm !== 'object' || Array.isArray(fm)) {
212
+ return { ok: false, file, errors: [{ kind: 'frontmatter_invalid', message: 'Frontmatter must be a YAML mapping' }], warnings, updates };
213
+ }
214
+
215
+ const schemaId = fm.schema_id;
216
+ if (!schemaId || typeof schemaId !== 'string') {
217
+ return { ok: false, file, errors: [{ kind: 'frontmatter_invalid', message: '`schema_id` field missing or not a string' }], warnings, updates };
218
+ }
219
+ const schemaPath = path.join(schemasDir, `${schemaId}.md`);
220
+ if (!fs.existsSync(schemaPath)) {
221
+ return { ok: false, file, schema_id: schemaId, errors: [{ kind: 'missing_schema', message: `Schema file not found: ${path.relative(root, schemaPath)}` }], warnings, updates };
222
+ }
223
+ const schemaContent = fs.readFileSync(schemaPath, 'utf8');
224
+ const schemaFmMatch = FRONTMATTER_RE.exec(schemaContent);
225
+ if (!schemaFmMatch) {
226
+ return { ok: false, file, schema_id: schemaId, errors: [{ kind: 'schema_invalid', message: `Schema ${schemaId}.md has no frontmatter` }], warnings, updates };
227
+ }
228
+ let schema;
229
+ try {
230
+ schema = parseYaml(schemaFmMatch[1]);
231
+ } catch (err) {
232
+ return { ok: false, file, schema_id: schemaId, errors: [{ kind: 'schema_invalid', message: `Schema YAML parse error: ${err.message}` }], warnings, updates };
233
+ }
234
+
235
+ const requiredFm = schema.required_frontmatter || {};
236
+ for (const [key, spec] of Object.entries(requiredFm)) {
237
+ const optional = spec && typeof spec === 'object' && spec.optional === true;
238
+ const value = fm[key];
239
+ if (value === undefined || value === null) {
240
+ if (!optional) {
241
+ errors.push({ kind: 'frontmatter_invalid', field: key, message: `Required frontmatter field '${key}' missing` });
242
+ }
243
+ continue;
244
+ }
245
+ if (spec && typeof spec === 'object') {
246
+ if (spec.equals !== undefined && value !== spec.equals) {
247
+ errors.push({ kind: 'frontmatter_invalid', field: key, message: `Field '${key}' must equal '${spec.equals}', got '${value}'` });
248
+ }
249
+ if (spec.type === 'integer' && !Number.isInteger(value)) {
250
+ errors.push({ kind: 'frontmatter_invalid', field: key, message: `Field '${key}' must be integer, got ${typeof value}` });
251
+ }
252
+ if (spec.type === 'enum' && Array.isArray(spec.values) && !spec.values.includes(value)) {
253
+ errors.push({ kind: 'frontmatter_invalid', field: key, message: `Field '${key}' must be one of [${spec.values.join(', ')}], got '${value}'` });
254
+ }
255
+ }
256
+ }
257
+
258
+ const requiredSections = Array.isArray(schema.required_sections) ? schema.required_sections : [];
259
+ const foundSections = [];
260
+ let m;
261
+ SECTION_RE.lastIndex = 0;
262
+ while ((m = SECTION_RE.exec(body)) !== null) {
263
+ foundSections.push(`## ${m[1]}`);
264
+ }
265
+ const sectionOrder = schema.section_order || 'strict';
266
+ if (sectionOrder === 'strict') {
267
+ let cursor = 0;
268
+ for (const required of requiredSections) {
269
+ const idx = foundSections.indexOf(required, cursor);
270
+ if (idx === -1) {
271
+ if (foundSections.includes(required)) {
272
+ errors.push({ kind: 'section_order_violation', section: required, message: `Section '${required}' appears out of declared order` });
273
+ } else {
274
+ errors.push({ kind: 'missing_section', section: required, message: `Required section '${required}' not found` });
275
+ }
276
+ } else {
277
+ cursor = idx + 1;
278
+ }
279
+ }
280
+ } else {
281
+ for (const required of requiredSections) {
282
+ if (!foundSections.includes(required)) {
283
+ errors.push({ kind: 'missing_section', section: required, message: `Required section '${required}' not found` });
284
+ }
285
+ }
286
+ }
287
+
288
+ const totalLines = original.split('\n').length;
289
+ if (typeof schema.hard_cap_lines === 'number' && totalLines > schema.hard_cap_lines) {
290
+ errors.push({
291
+ kind: 'hard_cap_exceeded',
292
+ lines: totalLines,
293
+ cap: schema.hard_cap_lines,
294
+ message: `File is ${totalLines} lines; hard cap is ${schema.hard_cap_lines}`,
295
+ });
296
+ }
297
+ const soft = schema.soft_target_lines;
298
+ if (Array.isArray(soft) && soft.length === 2 && (totalLines < soft[0] || totalLines > soft[1])) {
299
+ warnings.push({
300
+ kind: 'soft_cap_exceeded',
301
+ lines: totalLines,
302
+ soft_target: soft,
303
+ message: `File is ${totalLines} lines; soft target is ${soft[0]}-${soft[1]}`,
304
+ });
305
+ }
306
+
307
+ const fileLines = original.split('\n');
308
+ const lineByteOffsets = computeLineOffsets(original);
309
+
310
+ const hashedCitations = collectMatches(original, CITATION_HASHED_RE);
311
+ for (const citation of hashedCitations) {
312
+ const [, citedFile, citedLineStr, citedHash] = citation.match;
313
+ const citedLineNum = parseInt(citedLineStr, 10);
314
+ const onAckStaleLine = isOnAckStaleAnnotatedLine(citation.matchOffset, lineByteOffsets, fileLines);
315
+
316
+ const citedAbs = path.resolve(root, citedFile);
317
+ if (!fs.existsSync(citedAbs)) {
318
+ errors.push({
319
+ kind: 'missing_file_in_citation',
320
+ citation: citation.text,
321
+ message: `Cited file not found: ${citedFile}`,
322
+ });
323
+ continue;
324
+ }
325
+ const citedContent = fs.readFileSync(citedAbs, 'utf8');
326
+ const recomputed = computeContextHash(citedContent, citedLineNum);
327
+ if (recomputed === null) {
328
+ errors.push({
329
+ kind: 'stale_citation',
330
+ citation: citation.text,
331
+ message: `Cited line ${citedLineNum} is past end of ${citedFile}`,
332
+ });
333
+ continue;
334
+ }
335
+ if (recomputed !== citedHash) {
336
+ const finding = {
337
+ kind: 'stale_citation',
338
+ citation: citation.text,
339
+ expected: citedHash,
340
+ actual: recomputed,
341
+ message: `Hash mismatch: declared ${citedHash}, recomputed ${recomputed}`,
342
+ };
343
+ if (onAckStaleLine) {
344
+ warnings.push({ ...finding, kind: 'stale_citation_acked' });
345
+ } else {
346
+ errors.push(finding);
347
+ }
348
+ }
349
+ }
350
+
351
+ const hashedRanges = hashedCitations.map((c) => [c.matchOffset, c.matchOffset + c.text.length]);
352
+ let modifiedBody = body;
353
+ let modifiedFm = fmText;
354
+ const bareCitations = collectMatches(original, CITATION_BARE_RE).filter((c) => !overlaps(c.matchOffset, c.matchOffset + c.text.length, hashedRanges));
355
+ for (const citation of bareCitations) {
356
+ const [, citedFile, citedLineStr] = citation.match;
357
+ const citedLineNum = parseInt(citedLineStr, 10);
358
+ const citedAbs = path.resolve(root, citedFile);
359
+ if (!fs.existsSync(citedAbs)) {
360
+ errors.push({
361
+ kind: 'missing_file_in_citation',
362
+ citation: citation.text,
363
+ message: `Cited file not found: ${citedFile}`,
364
+ });
365
+ continue;
366
+ }
367
+ const citedContent = fs.readFileSync(citedAbs, 'utf8');
368
+ const computed = computeContextHash(citedContent, citedLineNum);
369
+ if (computed === null) {
370
+ errors.push({
371
+ kind: 'missing_file_in_citation',
372
+ citation: citation.text,
373
+ message: `Cited line ${citedLineNum} past end of ${citedFile}`,
374
+ });
375
+ continue;
376
+ }
377
+ const replacement = `${citation.text}@${computed}`;
378
+ updates.push({
379
+ kind: 'citation_hash_backfilled',
380
+ before: citation.text,
381
+ after: replacement,
382
+ });
383
+ modifiedBody = modifiedBody.split(citation.text).join(replacement);
384
+ modifiedFm = modifiedFm.split(citation.text).join(replacement);
385
+ }
386
+
387
+ if (updates.length > 0) {
388
+ const newContent = `---\n${modifiedFm}\n---\n${modifiedBody}`;
389
+ fs.writeFileSync(absFile, newContent, 'utf8');
390
+ }
391
+
392
+ return {
393
+ ok: errors.length === 0,
394
+ file,
395
+ schema_id: schemaId,
396
+ errors,
397
+ warnings,
398
+ updates,
399
+ };
400
+ }
401
+
402
+ // ---- Helpers ---------------------------------------------------------------
403
+
404
+ function computeContextHash(content, line) {
405
+ const lines = content.split('\n');
406
+ if (line < 1 || line > lines.length) return null;
407
+ const window = [];
408
+ for (let i = line - 2; i <= line + 2; i++) {
409
+ if (i < 1 || i > lines.length) {
410
+ window.push('');
411
+ } else {
412
+ window.push(lines[i - 1]);
413
+ }
414
+ }
415
+ const text = window.join('\n');
416
+ const hash = crypto.createHash('sha256').update(text, 'utf8').digest('hex');
417
+ return hash.substring(0, 7);
418
+ }
419
+
420
+ function collectMatches(text, regex) {
421
+ const out = [];
422
+ regex.lastIndex = 0;
423
+ let m;
424
+ while ((m = regex.exec(text)) !== null) {
425
+ out.push({ match: m, text: m[0], matchOffset: m.index });
426
+ }
427
+ return out;
428
+ }
429
+
430
+ function overlaps(start, end, ranges) {
431
+ for (const [rs, re] of ranges) {
432
+ if (start < re && end > rs) return true;
433
+ }
434
+ return false;
435
+ }
436
+
437
+ function computeLineOffsets(text) {
438
+ const offsets = [0];
439
+ for (let i = 0; i < text.length; i++) {
440
+ if (text[i] === '\n') offsets.push(i + 1);
441
+ }
442
+ return offsets;
443
+ }
444
+
445
+ function isOnAckStaleAnnotatedLine(byteOffset, lineOffsets, fileLines) {
446
+ let lineIdx = 0;
447
+ for (let i = 0; i < lineOffsets.length; i++) {
448
+ if (lineOffsets[i] > byteOffset) break;
449
+ lineIdx = i;
450
+ }
451
+ const lineText = fileLines[lineIdx] ?? '';
452
+ return ACK_STALE_RE.test(lineText);
453
+ }
454
+
455
+ // ---- CLI -------------------------------------------------------------------
456
+
457
+ function parseCliArgs() {
458
+ const { values } = parseArgs({
459
+ options: {
460
+ file: { type: 'string' },
461
+ schemas: { type: 'string' },
462
+ root: { type: 'string' },
463
+ },
464
+ strict: false,
465
+ });
466
+ if (!values.file || !values.schemas || !values.root) {
467
+ process.stderr.write('Usage: lint.mjs --file <path> --schemas <dir> --root <dir>\n');
468
+ process.exit(2);
469
+ }
470
+ return values;
471
+ }
472
+
473
+ function main() {
474
+ const { file, schemas, root } = parseCliArgs();
475
+ let result;
476
+ try {
477
+ result = lint({ file, schemasDir: schemas, root });
478
+ } catch (err) {
479
+ process.stdout.write(JSON.stringify({ ok: false, errors: [{ kind: 'internal_error', message: String(err?.message ?? err) }] }, null, 2) + '\n');
480
+ process.exit(2);
481
+ }
482
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
483
+ process.exit(result.ok ? 0 : 1);
484
+ }
485
+
486
+ if (import.meta.url === `file://${process.argv[1]}` || process.argv[1].endsWith('lint.mjs')) {
487
+ main();
488
+ }