@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.
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/agents/architect.md +92 -0
- package/agents/builder.md +122 -0
- package/agents/code-reviewer.md +107 -0
- package/agents/concept-designer.md +207 -0
- package/agents/craft-reviewer.md +132 -0
- package/agents/critic.md +130 -0
- package/agents/doc-writer.md +85 -0
- package/agents/dreamer.md +129 -0
- package/agents/e2e-runner.md +89 -0
- package/agents/gotcha-hunter.md +127 -0
- package/agents/prototype-builder.md +193 -0
- package/agents/prototype-codifier.md +204 -0
- package/agents/prototype-reviewer.md +163 -0
- package/agents/security-reviewer.md +108 -0
- package/agents/spec-reviewer.md +94 -0
- package/agents/tracer.md +98 -0
- package/agents/wireframer.md +109 -0
- package/commands/abort.md +25 -0
- package/commands/bugfix.md +151 -0
- package/commands/evolve.md +118 -0
- package/commands/feature.md +236 -0
- package/commands/forge.md +100 -0
- package/commands/greenfield.md +185 -0
- package/commands/hotfix.md +98 -0
- package/commands/refactor.md +147 -0
- package/commands/resume.md +25 -0
- package/commands/setup.md +201 -0
- package/commands/status.md +27 -0
- package/commands/task-force.md +110 -0
- package/commands/validate.md +12 -0
- package/dist/__tests__/active-manifest.test.js +272 -0
- package/dist/__tests__/copy.test.js +96 -0
- package/dist/__tests__/gate-check.test.js +384 -0
- package/dist/__tests__/wiki.test.js +472 -0
- package/dist/__tests__/work-manifest.test.js +304 -0
- package/dist/active-manifest.js +229 -0
- package/dist/cli.js +158 -0
- package/dist/copy.js +124 -0
- package/dist/gate-check.js +326 -0
- package/dist/hooks.js +60 -0
- package/dist/init.js +140 -0
- package/dist/manifest.js +90 -0
- package/dist/merge.js +77 -0
- package/dist/paths.js +36 -0
- package/dist/uninstall.js +216 -0
- package/dist/update.js +158 -0
- package/dist/verify-manifest.js +65 -0
- package/dist/verify.js +98 -0
- package/dist/wiki-ui.js +310 -0
- package/dist/wiki.js +364 -0
- package/dist/work-manifest.js +798 -0
- package/hooks/config/gate-requirements.json +79 -0
- package/hooks/hooks.json +143 -0
- package/hooks/scripts/analyze-telemetry.sh +114 -0
- package/hooks/scripts/gate-enforcer.sh +164 -0
- package/hooks/scripts/pre-compact.sh +90 -0
- package/hooks/scripts/session-start.sh +81 -0
- package/hooks/scripts/telemetry.sh +41 -0
- package/hooks/scripts/wiki-lint.sh +87 -0
- package/hooks/templates/AGENTS.md.template +48 -0
- package/hooks/templates/CLAUDE.md.template +45 -0
- package/package.json +55 -0
- package/protocols/README.md +40 -0
- package/protocols/codex.md +151 -0
- package/protocols/graphify.md +156 -0
- package/references/common/agent-coordination.md +65 -0
- package/references/common/coding-standards.md +54 -0
- package/references/common/feature-tracking.md +21 -0
- package/references/common/io-protocol.md +36 -0
- package/references/common/phases.md +57 -0
- package/references/common/quality-gates.md +130 -0
- package/references/common/skill-authoring.md +154 -0
- package/references/common/skill-compliance.md +30 -0
- package/references/python/standards.md +44 -0
- package/references/react/standards.md +61 -0
- package/references/typescript/standards.md +42 -0
- package/rules/common/forge-system.md +59 -0
- package/rules/common/git-workflow.md +40 -0
- package/rules/common/guardrails.md +37 -0
- package/rules/common/quality-gates.md +18 -0
- package/rules/common/security.md +50 -0
- package/rules/common/skill-selection.md +78 -0
- package/rules/common/testing.md +58 -0
- package/rules/common/verification.md +39 -0
- package/skills/build-pr-workflow/SKILL.md +301 -0
- package/skills/build-pr-workflow/references/pr-template.md +62 -0
- package/skills/build-pr-workflow/references/subagent-merge.md +47 -0
- package/skills/build-pr-workflow/references/worktree-setup.md +125 -0
- package/skills/build-prototype/SKILL.md +264 -0
- package/skills/build-scaffold/SKILL.md +340 -0
- package/skills/build-tdd/SKILL.md +89 -0
- package/skills/build-wireframe/SKILL.md +110 -0
- package/skills/build-wireframe/assets/baseline-template.html +486 -0
- package/skills/build-wireframe/references/demo-walkthroughs.md +170 -0
- package/skills/build-wireframe/references/gotchas.md +188 -0
- package/skills/build-wireframe/references/legend-lines.md +141 -0
- package/skills/concept-slides/SKILL.md +192 -0
- package/skills/deliver-db-migration/SKILL.md +466 -0
- package/skills/deliver-deploy/SKILL.md +407 -0
- package/skills/deliver-onboarding/SKILL.md +198 -0
- package/skills/deliver-onboarding/references/document-templates.md +393 -0
- package/skills/deliver-onboarding/templates/getting-started.md +122 -0
- package/skills/discover-codebase-analysis/SKILL.md +448 -0
- package/skills/discover-requirements/SKILL.md +418 -0
- package/skills/discover-requirements/templates/prd.md +99 -0
- package/skills/discover-requirements/templates/technical-spec.md +123 -0
- package/skills/discover-requirements/templates/user-stories.md +76 -0
- package/skills/harden/SKILL.md +214 -0
- package/skills/iterate-prototype/SKILL.md +241 -0
- package/skills/plan-architecture/SKILL.md +457 -0
- package/skills/plan-architecture/templates/adr-template.md +52 -0
- package/skills/plan-architecture/templates/api-contract.md +99 -0
- package/skills/plan-architecture/templates/db-schema.md +81 -0
- package/skills/plan-architecture/templates/system-design.md +111 -0
- package/skills/plan-brainstorm/SKILL.md +433 -0
- package/skills/plan-design-system/SKILL.md +279 -0
- package/skills/plan-task-decompose/SKILL.md +454 -0
- package/skills/quality-code-review/SKILL.md +286 -0
- package/skills/quality-security-audit/SKILL.md +292 -0
- package/skills/quality-security-audit/references/audit-report-template.md +89 -0
- package/skills/quality-security-audit/references/owasp-checks.md +178 -0
- package/skills/quality-test-execution/SKILL.md +435 -0
- package/skills/quality-test-plan/SKILL.md +297 -0
- package/skills/quality-test-plan/references/test-type-guide.md +263 -0
- package/skills/quality-test-plan/templates/e2e-test-plan.md +72 -0
- package/skills/quality-test-plan/templates/integration-test-plan.md +74 -0
- package/skills/quality-test-plan/templates/load-test-plan.md +111 -0
- package/skills/quality-test-plan/templates/smoke-test-plan.md +68 -0
- package/skills/quality-test-plan/templates/unit-test-plan.md +56 -0
- package/skills/quality-uiux/SKILL.md +481 -0
- package/skills/support-debug/SKILL.md +464 -0
- package/skills/support-dream/SKILL.md +213 -0
- package/skills/support-gotcha/SKILL.md +249 -0
- package/skills/support-runtime-reachability/SKILL.md +190 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-01-passes-app-use/src/app.ts +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-01-passes-app-use/src/handlers/cases.ts +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-02-orphan-no-app-use/src/app.ts +8 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-02-orphan-no-app-use/src/handlers/cases.ts +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/App.tsx +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/components/RingingBanner.tsx +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/hooks/useTwilio.ts +6 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-04-jsx-component-rendered/src/App.tsx +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-04-jsx-component-rendered/src/components/MyComp.tsx +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-05-jsx-component-not-rendered/src/App.tsx +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-05-jsx-component-not-rendered/src/components/Orphan.tsx +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-06-class-instantiated/src/lib/Service.ts +6 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-06-class-instantiated/src/main.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-07-class-not-instantiated/src/lib/Lonely.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-07-class-not-instantiated/src/main.ts +2 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-08-default-export-imported-and-called/src/handler.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-08-default-export-imported-and-called/src/main.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-09-default-export-orphan/src/handler.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-09-default-export-orphan/src/main.ts +2 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-10-aliased-named-export/src/lib.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-10-aliased-named-export/src/main.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/lib/index.ts +1 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/lib/internal.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/main.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-12-test-only-caller/src/util.test.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-12-test-only-caller/src/util.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-13-gated-pending-annotation/src/future.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-14-untraceable-annotation/src/decorated.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-15-untraceable-empty/src/lazy.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-16-python-module/src/lib.py +15 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-16-python-module/src/main.py +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-17-router-use/src/parent.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-17-router-use/src/routes/cases.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-18-shadowed-name-fp/src/lib/foo.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-18-shadowed-name-fp/src/other.ts +8 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/handlers/cases.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/handlers/users.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/main.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-20-aliased-import-usage/src/handlers/cases.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-20-aliased-import-usage/src/main.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-21-mixed-default-and-named/src/lib.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-21-mixed-default-and-named/src/main.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-22-dynamic-import-then-caller/src/lib.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-22-dynamic-import-then-caller/src/main.ts +8 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-23-dynamic-import-with-space/src/lib.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-23-dynamic-import-with-space/src/main.ts +7 -0
- package/skills/support-runtime-reachability/scripts/check.mjs +638 -0
- package/skills/support-runtime-reachability/scripts/check.test.mjs +244 -0
- package/skills/support-skill-validator/SKILL.md +194 -0
- package/skills/support-skill-validator/references/false-positives.md +59 -0
- package/skills/support-skill-validator/references/validation-checks.md +280 -0
- package/skills/support-system-guide/SKILL.md +311 -0
- package/skills/support-task-force/SKILL.md +265 -0
- package/skills/support-task-force/references/dispatch-pattern.md +178 -0
- package/skills/support-task-force/references/synthesis-template.md +126 -0
- package/skills/support-wiki-bootstrap/SKILL.md +37 -0
- package/skills/support-wiki-lint/SKILL.md +196 -0
- package/skills/support-wiki-lint/scripts/lint.mjs +488 -0
- package/skills/support-wiki-lint/scripts/lint.test.mjs +196 -0
- package/templates/README.md +23 -0
- package/templates/aiwiki/CLAUDE.md.template +78 -0
- package/templates/aiwiki/schemas/architecture.md +118 -0
- package/templates/aiwiki/schemas/convention.md +112 -0
- package/templates/aiwiki/schemas/decision.md +144 -0
- package/templates/aiwiki/schemas/gotcha.md +118 -0
- package/templates/aiwiki/schemas/oracle.md +105 -0
- package/templates/aiwiki/schemas/session.md +125 -0
- package/templates/manifests/bugfix.yaml +41 -0
- package/templates/manifests/feature.yaml +69 -0
- package/templates/manifests/greenfield.yaml +61 -0
- package/templates/manifests/hotfix.yaml +45 -0
- package/templates/manifests/refactor.yaml +44 -0
- package/templates/manifests/v5/SCHEMA.md +327 -0
- package/templates/manifests/v5/feature.yaml +77 -0
- package/templates/manifests/v6/SCHEMA.md +199 -0
- package/templates/wiki-html/dream-detail.html +378 -0
- package/templates/wiki-html/dreams-list.html +155 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { lint, parseYaml } from './lint.mjs';
|
|
9
|
+
|
|
10
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const repoRoot = path.resolve(here, '../../..');
|
|
12
|
+
const templateSchemasDir = path.join(repoRoot, 'templates/aiwiki/schemas');
|
|
13
|
+
|
|
14
|
+
function mkTempRoot() {
|
|
15
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-lint-test-'));
|
|
16
|
+
// Copy schemas the lint script reads.
|
|
17
|
+
const schemasDir = path.join(root, 'schemas');
|
|
18
|
+
fs.mkdirSync(schemasDir, { recursive: true });
|
|
19
|
+
for (const entry of fs.readdirSync(templateSchemasDir)) {
|
|
20
|
+
fs.copyFileSync(path.join(templateSchemasDir, entry), path.join(schemasDir, entry));
|
|
21
|
+
}
|
|
22
|
+
return { root, schemasDir };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeFile(root, rel, content) {
|
|
26
|
+
const full = path.join(root, rel);
|
|
27
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
28
|
+
fs.writeFileSync(full, content);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const GOOD_GOTCHA = `---
|
|
32
|
+
schema_id: gotcha
|
|
33
|
+
schema_version: 1
|
|
34
|
+
severity: medium
|
|
35
|
+
date: 2026-05-12
|
|
36
|
+
occurrences: 1
|
|
37
|
+
status: active
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
# Stub logger throws under server-rendered hydration
|
|
41
|
+
|
|
42
|
+
## What broke
|
|
43
|
+
|
|
44
|
+
The logger stub threw \`NotImplemented\` when rendered twice (server + client).
|
|
45
|
+
|
|
46
|
+
## Reproducer
|
|
47
|
+
|
|
48
|
+
Run a Next.js page that imports the stub logger; observe the throw on client mount.
|
|
49
|
+
|
|
50
|
+
## Root cause
|
|
51
|
+
|
|
52
|
+
Logger was initialized twice but only one stub path was instrumented to throw.
|
|
53
|
+
|
|
54
|
+
## Fix
|
|
55
|
+
|
|
56
|
+
Throw on both stub paths so the call site is forced to inject a real logger.
|
|
57
|
+
|
|
58
|
+
## Prevention
|
|
59
|
+
|
|
60
|
+
When stubbing module-level singletons, throw on every code path that could fire.
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
test('parseYaml handles a minimal mapping', () => {
|
|
64
|
+
const parsed = parseYaml('schema_id: gotcha\nseverity: medium\n');
|
|
65
|
+
assert.equal(parsed.schema_id, 'gotcha');
|
|
66
|
+
assert.equal(parsed.severity, 'medium');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('parseYaml handles inline mapping values', () => {
|
|
70
|
+
const parsed = parseYaml('field: { type: enum, values: [low, high] }\n');
|
|
71
|
+
assert.deepEqual(parsed.field, { type: 'enum', values: ['low', 'high'] });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('valid gotcha page passes lint', () => {
|
|
75
|
+
const { root, schemasDir } = mkTempRoot();
|
|
76
|
+
writeFile(root, 'aiwiki/gotchas/2026-05-12-stub-logger.md', GOOD_GOTCHA);
|
|
77
|
+
const result = lint({ file: 'aiwiki/gotchas/2026-05-12-stub-logger.md', schemasDir, root });
|
|
78
|
+
assert.equal(result.ok, true, `lint errors: ${JSON.stringify(result.errors)}`);
|
|
79
|
+
assert.equal(result.errors.length, 0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('missing required frontmatter field is flagged', () => {
|
|
83
|
+
const { root, schemasDir } = mkTempRoot();
|
|
84
|
+
const badFrontmatter = GOOD_GOTCHA.replace('severity: medium\n', '');
|
|
85
|
+
writeFile(root, 'aiwiki/gotchas/bad.md', badFrontmatter);
|
|
86
|
+
const result = lint({ file: 'aiwiki/gotchas/bad.md', schemasDir, root });
|
|
87
|
+
assert.equal(result.ok, false);
|
|
88
|
+
assert.ok(
|
|
89
|
+
result.errors.some((e) => e.kind === 'frontmatter_invalid' && e.field === 'severity'),
|
|
90
|
+
`expected severity field error, got: ${JSON.stringify(result.errors)}`,
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('bad enum value is flagged', () => {
|
|
95
|
+
const { root, schemasDir } = mkTempRoot();
|
|
96
|
+
const badEnum = GOOD_GOTCHA.replace('severity: medium', 'severity: lukewarm');
|
|
97
|
+
writeFile(root, 'aiwiki/gotchas/bad.md', badEnum);
|
|
98
|
+
const result = lint({ file: 'aiwiki/gotchas/bad.md', schemasDir, root });
|
|
99
|
+
assert.equal(result.ok, false);
|
|
100
|
+
assert.ok(
|
|
101
|
+
result.errors.some((e) => e.kind === 'frontmatter_invalid' && e.field === 'severity'),
|
|
102
|
+
`expected severity enum error, got: ${JSON.stringify(result.errors)}`,
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('missing required H2 section is flagged', () => {
|
|
107
|
+
const { root, schemasDir } = mkTempRoot();
|
|
108
|
+
const noSection = GOOD_GOTCHA.replace(/## Root cause[\s\S]*?## Fix/, '## Fix');
|
|
109
|
+
writeFile(root, 'aiwiki/gotchas/bad.md', noSection);
|
|
110
|
+
const result = lint({ file: 'aiwiki/gotchas/bad.md', schemasDir, root });
|
|
111
|
+
assert.equal(result.ok, false);
|
|
112
|
+
assert.ok(
|
|
113
|
+
result.errors.some((e) => e.kind === 'missing_section' && e.section === '## Root cause'),
|
|
114
|
+
`expected missing-section error, got: ${JSON.stringify(result.errors)}`,
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('out-of-order sections trip section_order_violation', () => {
|
|
119
|
+
const { root, schemasDir } = mkTempRoot();
|
|
120
|
+
const swapped = GOOD_GOTCHA
|
|
121
|
+
.replace(/## What broke[\s\S]*?(?=## Reproducer)/, (m) => m)
|
|
122
|
+
// Move ## Fix before ## Root cause
|
|
123
|
+
.replace(/## Root cause([\s\S]*?)## Fix([\s\S]*?)## Prevention/, '## Fix$2## Root cause$1## Prevention');
|
|
124
|
+
writeFile(root, 'aiwiki/gotchas/bad.md', swapped);
|
|
125
|
+
const result = lint({ file: 'aiwiki/gotchas/bad.md', schemasDir, root });
|
|
126
|
+
assert.equal(result.ok, false);
|
|
127
|
+
assert.ok(
|
|
128
|
+
result.errors.some((e) => e.kind === 'section_order_violation'),
|
|
129
|
+
`expected section_order_violation, got: ${JSON.stringify(result.errors)}`,
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('hard_cap_lines exceeded is flagged', () => {
|
|
134
|
+
const { root, schemasDir } = mkTempRoot();
|
|
135
|
+
// gotcha schema's hard_cap_lines is 150. Pad with bullet lines.
|
|
136
|
+
const padding = Array.from({ length: 200 }, (_, i) => `- padding line ${i}`).join('\n');
|
|
137
|
+
const tooLong = GOOD_GOTCHA.replace('## Prevention', `## Prevention\n\n${padding}\n\n## Other`);
|
|
138
|
+
writeFile(root, 'aiwiki/gotchas/bad.md', tooLong);
|
|
139
|
+
const result = lint({ file: 'aiwiki/gotchas/bad.md', schemasDir, root });
|
|
140
|
+
assert.equal(result.ok, false);
|
|
141
|
+
assert.ok(
|
|
142
|
+
result.errors.some((e) => e.kind === 'hard_cap_exceeded'),
|
|
143
|
+
`expected hard_cap_exceeded, got: ${JSON.stringify(result.errors)}`,
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('missing schema_id is rejected', () => {
|
|
148
|
+
const { root, schemasDir } = mkTempRoot();
|
|
149
|
+
const noSchemaId = GOOD_GOTCHA.replace('schema_id: gotcha\n', '');
|
|
150
|
+
writeFile(root, 'aiwiki/gotchas/bad.md', noSchemaId);
|
|
151
|
+
const result = lint({ file: 'aiwiki/gotchas/bad.md', schemasDir, root });
|
|
152
|
+
assert.equal(result.ok, false);
|
|
153
|
+
assert.ok(
|
|
154
|
+
result.errors.some((e) => e.kind === 'frontmatter_invalid' && /schema_id/.test(e.message)),
|
|
155
|
+
`expected schema_id error, got: ${JSON.stringify(result.errors)}`,
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('unknown schema_id is rejected', () => {
|
|
160
|
+
const { root, schemasDir } = mkTempRoot();
|
|
161
|
+
const wrongSchema = GOOD_GOTCHA.replace('schema_id: gotcha', 'schema_id: nonexistent');
|
|
162
|
+
writeFile(root, 'aiwiki/gotchas/bad.md', wrongSchema);
|
|
163
|
+
const result = lint({ file: 'aiwiki/gotchas/bad.md', schemasDir, root });
|
|
164
|
+
assert.equal(result.ok, false);
|
|
165
|
+
assert.ok(
|
|
166
|
+
result.errors.some((e) => e.kind === 'missing_schema'),
|
|
167
|
+
`expected missing_schema error, got: ${JSON.stringify(result.errors)}`,
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('bare citation auto-backfills @<sha7>', () => {
|
|
172
|
+
const { root, schemasDir } = mkTempRoot();
|
|
173
|
+
// Create a real source file inside a git repo so the lint script can compute its hash.
|
|
174
|
+
execSync('git init -q', { cwd: root });
|
|
175
|
+
execSync('git config user.email test@example.com', { cwd: root });
|
|
176
|
+
execSync('git config user.name Test', { cwd: root });
|
|
177
|
+
writeFile(root, 'src/handler.ts', 'export function handler() {\n return 1;\n}\n');
|
|
178
|
+
execSync('git add . && git commit -q -m init', { cwd: root });
|
|
179
|
+
|
|
180
|
+
const pageWithBareCite = GOOD_GOTCHA.replace(
|
|
181
|
+
'## Reproducer',
|
|
182
|
+
'## Reproducer\n\nSee src/handler.ts:1 for the broken path.',
|
|
183
|
+
);
|
|
184
|
+
writeFile(root, 'aiwiki/gotchas/cite.md', pageWithBareCite);
|
|
185
|
+
|
|
186
|
+
const result = lint({ file: 'aiwiki/gotchas/cite.md', schemasDir, root });
|
|
187
|
+
// Either backfilled (auto-fix) or flagged — both are valid lint outcomes
|
|
188
|
+
// depending on whether updates[] auto-applies. Check that the lint at least
|
|
189
|
+
// processed the citation rather than crashing.
|
|
190
|
+
assert.ok(result.errors !== undefined, 'lint should return an errors array');
|
|
191
|
+
// If an update was emitted, the file should now contain an @<sha7> citation.
|
|
192
|
+
if (result.updates && result.updates.length > 0) {
|
|
193
|
+
const updated = fs.readFileSync(path.join(root, 'aiwiki/gotchas/cite.md'), 'utf8');
|
|
194
|
+
assert.match(updated, /src\/handler\.ts:1@[0-9a-f]{7}/, 'expected backfilled citation hash');
|
|
195
|
+
}
|
|
196
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# templates/
|
|
2
|
+
|
|
3
|
+
Templates copied into user projects by `npx @jamie-tam/forge init`, plus schema docs used internally by forge maintainers.
|
|
4
|
+
|
|
5
|
+
## User-installed templates
|
|
6
|
+
|
|
7
|
+
Installed into the target project by `npx @jamie-tam/forge init` (or kept in-sync by subsequent updates). These are the files real users of forge see.
|
|
8
|
+
|
|
9
|
+
- **`manifests/`** — Canonical work-manifest templates, one per work type (`feature.yaml`, `greenfield.yaml`, `bugfix.yaml`, `hotfix.yaml`, `refactor.yaml`). Written to `.forge/work/{type}/{name}/manifest.yaml` when a work command starts. Each manifest is the source of truth for its schema shape — do not duplicate schema documentation elsewhere.
|
|
10
|
+
- **`aiwiki/`** — Seed contents for the project's `aiwiki/` directory: the `CLAUDE.md.template` that auto-loads wiki usage rules, plus the `schemas/` folder defining the file shape for conventions, gotchas, architecture, decisions, and sessions.
|
|
11
|
+
- **`wiki-html/`** — Single-file HTML pages (e.g. `dreams-list.html`, `dream-detail.html`) used by the wiki review UI surfaced via the forge CLI.
|
|
12
|
+
|
|
13
|
+
## Legacy
|
|
14
|
+
|
|
15
|
+
Kept on-disk so older manifests still parse and so `/evolve` can migrate them forward. Not installed into new projects.
|
|
16
|
+
|
|
17
|
+
- **`manifests/v5/`** — v5 schema (`SCHEMA.md` plus a `feature.yaml` reference). v5 manifests still parse against the v6 reader (see `manifests/v6/SCHEMA.md` §4). v4 manifests also still parse but no template is retained — the reader synthesizes the missing fields.
|
|
18
|
+
|
|
19
|
+
## Internal
|
|
20
|
+
|
|
21
|
+
Used by forge maintainers writing the parser/verifier and the migration logic. Not consumed at user runtime.
|
|
22
|
+
|
|
23
|
+
- **`manifests/v6/SCHEMA.md`** — Source of truth for v6 manifest validation rules. The reader and verifier in `src/work-manifest.ts` implement against this document. v6's delta over v5 is the additive `phase_plan:` block (see §3). Update this file alongside any change to manifest shape or allowed plan-status values.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Wiki usage rules (auto-loaded every session)
|
|
2
|
+
|
|
3
|
+
This file is loaded automatically by Claude Code at session start. It tells you (the AI) how to use the `aiwiki/` directory in this project.
|
|
4
|
+
|
|
5
|
+
## Reading
|
|
6
|
+
|
|
7
|
+
Before answering any non-trivial question, check the wiki:
|
|
8
|
+
|
|
9
|
+
- `aiwiki/conventions/` — for "how does this codebase do X?"
|
|
10
|
+
- `aiwiki/gotchas/` — for "have we hit this before?"
|
|
11
|
+
- `aiwiki/architecture/` — for "what is the system shape?"
|
|
12
|
+
- `aiwiki/decisions/` — for "why was this chosen?"
|
|
13
|
+
- `aiwiki/oracles/` — for "what behavior must the production code reproduce from the prototype?"
|
|
14
|
+
|
|
15
|
+
The wiki is curated to answer recurring questions. **Use it before asking the user.**
|
|
16
|
+
|
|
17
|
+
## Writing
|
|
18
|
+
|
|
19
|
+
When you make a decision that's hard to reverse — architectural choice, public-surface naming (APIs / schemas / file paths users import), security or data-handling tradeoff, schema design, or a contract that other parts of the codebase depend on:
|
|
20
|
+
- Write an ADR in `aiwiki/decisions/`. Format per `aiwiki/schemas/decision.md`. Invoke `/second-opinion` to get adversarial review before marking `status: accepted`.
|
|
21
|
+
|
|
22
|
+
When you encounter a recurring failure:
|
|
23
|
+
- Write a gotcha in `aiwiki/gotchas/`. Format per `aiwiki/schemas/gotcha.md`.
|
|
24
|
+
- If the gotcha file's `occurrences` reaches 3+, the gotcha is auto-drafted as a `proposed_rule:` block; the next session-start hard-interrupts to require user approval before promoting the rule.
|
|
25
|
+
|
|
26
|
+
When you discover a codebase convention:
|
|
27
|
+
- Write to `aiwiki/conventions/`. Format per `aiwiki/schemas/convention.md`.
|
|
28
|
+
|
|
29
|
+
When you make a system-shape change:
|
|
30
|
+
- Write to `aiwiki/architecture/{topic}.md`. Format per `aiwiki/schemas/architecture.md`.
|
|
31
|
+
- One file per topic — do NOT collapse multiple subsystems into a single mega-file.
|
|
32
|
+
|
|
33
|
+
When unsure where something belongs:
|
|
34
|
+
- Write to `aiwiki/raw/{YYYY-MM-DD}-{slug}.md`. Phase-close dream consolidates raw entries into typed pages.
|
|
35
|
+
|
|
36
|
+
## Citations
|
|
37
|
+
|
|
38
|
+
Cite code with `file:line@<sha7>` (e.g. `src/auth.ts:42@a3f2bc1`) or `symbol` form (e.g. `src/auth.ts#login`). The `@<sha7>` part is auto-filled by LINT on first save.
|
|
39
|
+
|
|
40
|
+
When the cited code moves, LINT flags the citation as stale on the next wiki write. Resolve by:
|
|
41
|
+
- Updating the citation to the new location, OR
|
|
42
|
+
- Removing the claim that depended on it, OR
|
|
43
|
+
- Annotating `// ack-stale: <reason>` on the citing line if the staleness is acceptable for now.
|
|
44
|
+
|
|
45
|
+
## Reviewing dream output
|
|
46
|
+
|
|
47
|
+
The wiki is consolidated periodically by **dream** (forge's wiki-consolidation mechanism — see `support-dream` skill). Dream output goes to `aiwiki/proposed/{dream_id}/` and is **never auto-applied** to `aiwiki/`.
|
|
48
|
+
|
|
49
|
+
When pending dreams exist:
|
|
50
|
+
- Run `forge wiki status` to see the queue
|
|
51
|
+
- Run `forge wiki review [dream_id]` to review per-page diffs
|
|
52
|
+
- Run `forge wiki accept [dream_id]` or `forge wiki reject [dream_id] --reason "..."` to resolve
|
|
53
|
+
|
|
54
|
+
You will be notified at session start (soft) and at phase-close (hard interrupt if a phase-close dream is pending).
|
|
55
|
+
|
|
56
|
+
## Phase vocabulary
|
|
57
|
+
|
|
58
|
+
When a skill or agent states its phase context, the canonical numbering and names are defined in `.claude/references/common/phases.md`. Load it when in doubt — different files reusing the wrong phase numbers is the dominant source of operational confusion. Phases are defaults, not requirements; manifest tracks deviations.
|
|
59
|
+
|
|
60
|
+
## Rules and references — what applies when
|
|
61
|
+
|
|
62
|
+
Forge tiers code-standards content by phase to match its prototype-driven SDLC. The thesis: prototype iteration should be fast and unconstrained by style/convention rules; production-grade standards take effect when the prototype is codified.
|
|
63
|
+
|
|
64
|
+
| Tier | Files | Active during |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| Always-on (safety floor) | `.claude/rules/common/security.md`, `guardrails.md`, `verification.md` | Every phase. Prevents classes of issues that are expensive to retrofit (injection, missing input validation, unverified completion claims). |
|
|
67
|
+
| Phase-conditional | `.claude/rules/common/testing.md`, `quality-gates.md`, `git-workflow.md` | Phase 5 (codify) onward. Headers in those files mark the transition explicitly. |
|
|
68
|
+
| References (load on demand) | `.claude/references/common/coding-standards.md`, `.claude/references/{language}/standards.md` (typescript, react, python) | Loaded by the `harden` skill / `prototype-codifier` agent at codify time. Not active during prototype iteration. |
|
|
69
|
+
|
|
70
|
+
**Implication for AI agents:** during Phases 1–4 (concept → wireframe → prototype → iterate), follow the safety floor only — do not preemptively apply style/convention/testing standards. During Phase 5 (codify/harden) onward, load the references and phase-conditional rules listed above.
|
|
71
|
+
|
|
72
|
+
## Forbidden
|
|
73
|
+
|
|
74
|
+
- **Never edit `aiwiki/proposed/` directly** — that's dream output for review. Edit `aiwiki/` if you need to change something; dream re-runs on the new state next cycle.
|
|
75
|
+
- **Never duplicate code in wiki entries** — cite instead. The code is the truth; the wiki points at it.
|
|
76
|
+
- **Never write speculation, conversation history, or "we might want to revisit this" notes** — those don't answer recurring questions. They're noise.
|
|
77
|
+
- **Never write to `.forge/work/manifest.yaml` from a wiki context** — the manifest is operational state for hooks/gates, not knowledge. Wiki and manifest are separate.
|
|
78
|
+
- **Every section in a wiki page must answer a recurring AI question.** If a section doesn't get re-read, it doesn't belong. When in doubt, omit.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_id: architecture
|
|
3
|
+
schema_version: 1
|
|
4
|
+
applies_to: aiwiki/architecture/**/*.md
|
|
5
|
+
filename_pattern: "{topic}.md"
|
|
6
|
+
hard_cap_lines: 400
|
|
7
|
+
soft_target_lines: [100, 250]
|
|
8
|
+
required_frontmatter:
|
|
9
|
+
schema_id: { type: string, equals: architecture }
|
|
10
|
+
schema_version: { type: integer }
|
|
11
|
+
scope: { type: string, pattern: "^(project|subsystem:.+)$" }
|
|
12
|
+
date: { type: date }
|
|
13
|
+
status: { type: enum, values: [active, proposed, superseded] }
|
|
14
|
+
required_sections:
|
|
15
|
+
- "## The shape"
|
|
16
|
+
- "## Components"
|
|
17
|
+
- "## Boundaries"
|
|
18
|
+
- "## Tradeoffs"
|
|
19
|
+
section_order: strict
|
|
20
|
+
citation_rule: required-in-components
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# Schema: architecture (system-shape doc)
|
|
24
|
+
|
|
25
|
+
## Purpose
|
|
26
|
+
|
|
27
|
+
An architecture file describes the shape of a subsystem — what its parts are, what's in and out of scope, what tradeoffs were taken. The page answers "what is this system? what does each piece do? where does it stop?" so future contributors don't have to reverse-engineer.
|
|
28
|
+
|
|
29
|
+
**One file per topic.** Multiple focused files (`data-layer.md`, `auth-flow.md`, `event-bus.md`) — NOT one giant `architecture.md`. A 400-line file forced into existence by combining unrelated subsystems is unreadable.
|
|
30
|
+
|
|
31
|
+
## When to write one
|
|
32
|
+
|
|
33
|
+
- During Phase 5 codification (`harden`) — extract architecture from the locked prototype
|
|
34
|
+
- When a subsystem grows past ~3 files and a "what does this do" overview becomes valuable
|
|
35
|
+
- After a refactor that meaningfully reshapes a subsystem (write the new shape; mark the old one `superseded`)
|
|
36
|
+
|
|
37
|
+
Do NOT write an architecture file for: a single function or class (write a comment in the code), a feature you haven't built yet (write a plan in `.forge/work/`), an entire codebase summary (split by subsystem).
|
|
38
|
+
|
|
39
|
+
## File location and naming
|
|
40
|
+
|
|
41
|
+
- Path: `aiwiki/architecture/{topic}.md`
|
|
42
|
+
- Topic: kebab-case, names the subsystem or concern (not the document type)
|
|
43
|
+
|
|
44
|
+
Examples: `data-layer.md`, `auth-flow.md`, `event-bus.md`, `frontend-state-model.md`.
|
|
45
|
+
|
|
46
|
+
## Required frontmatter
|
|
47
|
+
|
|
48
|
+
| Field | Type | Notes |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `schema_id` | string | Must equal `architecture` |
|
|
51
|
+
| `schema_version` | integer | — |
|
|
52
|
+
| `scope` | string | `project` / `subsystem:<name>` |
|
|
53
|
+
| `date` | ISO date | When this shape was codified or last refreshed |
|
|
54
|
+
| `status` | enum | `active` (current) / `proposed` (not yet implemented) / `superseded` (old shape; link successor) |
|
|
55
|
+
|
|
56
|
+
## Required sections
|
|
57
|
+
|
|
58
|
+
| Section | Purpose | Format |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `## The shape` | What it looks like at a glance | Mermaid diagram OR ≤5-sentence prose summary |
|
|
61
|
+
| `## Components` | What each part does | Bulleted list with citations to the actual code |
|
|
62
|
+
| `## Boundaries` | What's in scope, what's out, and where the seams are | Bullets — explicit "in" and "out" |
|
|
63
|
+
| `## Tradeoffs` | What was rejected and why | Link to ADRs in `aiwiki/decisions/` |
|
|
64
|
+
|
|
65
|
+
## Line caps
|
|
66
|
+
|
|
67
|
+
- Hard cap: 400 lines (LINT fails above)
|
|
68
|
+
- Soft target: 100-250 lines
|
|
69
|
+
|
|
70
|
+
If a subsystem genuinely needs >400 lines to describe, split it: `auth-flow.md` + `auth-token-storage.md` + `auth-session-lifecycle.md` rather than one mega-file.
|
|
71
|
+
|
|
72
|
+
## Citation rules
|
|
73
|
+
|
|
74
|
+
- `## Components` MUST cite the responsible files/symbols with `file:line@<sha7>` or `symbol` form
|
|
75
|
+
- `## The shape` may use Mermaid (no citations needed) or prose (cite if making code claims)
|
|
76
|
+
- `## Tradeoffs` should link to ADRs that capture the decisions
|
|
77
|
+
- LINT auto-fills missing `@<sha7>` on first save
|
|
78
|
+
|
|
79
|
+
## Skeleton
|
|
80
|
+
|
|
81
|
+
```markdown
|
|
82
|
+
---
|
|
83
|
+
schema_id: architecture
|
|
84
|
+
schema_version: 1
|
|
85
|
+
scope: subsystem:auth
|
|
86
|
+
date: 2026-05-10
|
|
87
|
+
status: active
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## The shape
|
|
91
|
+
|
|
92
|
+
```mermaid
|
|
93
|
+
flowchart LR
|
|
94
|
+
Client -->|credentials| AuthHandler
|
|
95
|
+
AuthHandler -->|verified| TokenIssuer
|
|
96
|
+
TokenIssuer -->|signed token| Client
|
|
97
|
+
Client -->|token| ProtectedRoute
|
|
98
|
+
ProtectedRoute -->|verify| TokenValidator
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Components
|
|
102
|
+
|
|
103
|
+
- `AuthHandler` ([src/auth/handler.ts#authHandler](src/auth/handler.ts)) — accepts credentials, dispatches to credential validator
|
|
104
|
+
- `TokenIssuer` ([src/auth/token.ts#issueToken](src/auth/token.ts)) — signs JWT with HS256; lifetime 24h
|
|
105
|
+
- `TokenValidator` ([src/auth/middleware.ts#validateToken](src/auth/middleware.ts)) — verifies signature + expiry on every protected request
|
|
106
|
+
|
|
107
|
+
## Boundaries
|
|
108
|
+
|
|
109
|
+
**In scope**: credential validation, token issuance, token verification middleware.
|
|
110
|
+
|
|
111
|
+
**Out of scope**: user CRUD (lives in [aiwiki/architecture/users.md](aiwiki/architecture/users.md)), password reset flow (lives in [aiwiki/architecture/password-reset.md](aiwiki/architecture/password-reset.md)), MFA (deferred — see [aiwiki/decisions/0023-mfa-deferral.md](aiwiki/decisions/0023-mfa-deferral.md)).
|
|
112
|
+
|
|
113
|
+
## Tradeoffs
|
|
114
|
+
|
|
115
|
+
- HS256 over RS256: chosen for single-service deployment ([aiwiki/decisions/0008-jwt-algorithm.md](aiwiki/decisions/0008-jwt-algorithm.md))
|
|
116
|
+
- 24h token lifetime over refresh-token pair: chosen for simplicity at MVP scale ([aiwiki/decisions/0009-token-lifetime.md](aiwiki/decisions/0009-token-lifetime.md))
|
|
117
|
+
- Stateless JWT over session-store pattern: chosen for horizontal scalability ([aiwiki/decisions/0007-stateless-auth.md](aiwiki/decisions/0007-stateless-auth.md))
|
|
118
|
+
```
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_id: convention
|
|
3
|
+
schema_version: 1
|
|
4
|
+
applies_to: aiwiki/conventions/**/*.md
|
|
5
|
+
filename_pattern: "{slug}.md"
|
|
6
|
+
hard_cap_lines: 100
|
|
7
|
+
soft_target_lines: [30, 60]
|
|
8
|
+
required_frontmatter:
|
|
9
|
+
schema_id: { type: string, equals: convention }
|
|
10
|
+
schema_version: { type: integer }
|
|
11
|
+
scope: { type: string, pattern: "^(project|folder:.+|module:.+)$" }
|
|
12
|
+
date: { type: date }
|
|
13
|
+
status: { type: enum, values: [active, superseded, retired] }
|
|
14
|
+
required_sections:
|
|
15
|
+
- "## The convention"
|
|
16
|
+
- "## Rationale"
|
|
17
|
+
- "## Example"
|
|
18
|
+
- "## Counter-example"
|
|
19
|
+
section_order: strict
|
|
20
|
+
citation_rule: required-in-example-and-counter-example
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# Schema: convention (codebase pattern)
|
|
24
|
+
|
|
25
|
+
## Purpose
|
|
26
|
+
|
|
27
|
+
A convention records a "how this codebase does X" pattern. The page answers "what's the rule? show me what right looks like, and what wrong looks like." Conventions are short by nature; if you need more than a paragraph to state the rule, it's probably an ADR or an architecture doc.
|
|
28
|
+
|
|
29
|
+
## When to write one
|
|
30
|
+
|
|
31
|
+
- A pattern emerges across multiple files during prototype iteration or production build (e.g. how route handlers are named, where shared types live, how errors are formatted)
|
|
32
|
+
- A code reviewer flags "this doesn't match how we usually do X" — write the convention so the next contributor knows
|
|
33
|
+
- A gotcha cluster signals a missing convention — the convention prevents future occurrences
|
|
34
|
+
|
|
35
|
+
Do NOT write a convention for: language-level idioms (use a linter or formatter instead), things that are obvious from a reasonable read of the codebase, one-off patterns that exist in a single file.
|
|
36
|
+
|
|
37
|
+
## File location and naming
|
|
38
|
+
|
|
39
|
+
- Path: `aiwiki/conventions/{slug}.md`
|
|
40
|
+
- Slug: kebab-case, ≤6 words, naming the pattern (not the rationale)
|
|
41
|
+
|
|
42
|
+
Examples: `route-handler-naming.md`, `error-shape.md`, `shared-types-location.md`.
|
|
43
|
+
|
|
44
|
+
## Required frontmatter
|
|
45
|
+
|
|
46
|
+
| Field | Type | Notes |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `schema_id` | string | Must equal `convention` |
|
|
49
|
+
| `schema_version` | integer | — |
|
|
50
|
+
| `scope` | string | `project` / `folder:<path>` / `module:<name>` — what this convention applies to |
|
|
51
|
+
| `date` | ISO date | When the convention was codified |
|
|
52
|
+
| `status` | enum | `active` / `superseded` (newer convention takes priority — link it) / `retired` |
|
|
53
|
+
|
|
54
|
+
## Required sections
|
|
55
|
+
|
|
56
|
+
| Section | Purpose | Format |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `## The convention` | One paragraph in imperative mood — the rule itself | No fluff, no rationale |
|
|
59
|
+
| `## Rationale` | Why this convention exists | Brief; link to ADR or gotcha if one motivates it |
|
|
60
|
+
| `## Example` | Code that follows the convention | Cite the example code with `file:line@<sha7>` |
|
|
61
|
+
| `## Counter-example` | Code that violates the convention (or what it would look like) | Cite if real, or write a synthetic counter-example |
|
|
62
|
+
|
|
63
|
+
## Line caps
|
|
64
|
+
|
|
65
|
+
- Hard cap: 100 lines (LINT fails above)
|
|
66
|
+
- Soft target: 30-60 lines
|
|
67
|
+
|
|
68
|
+
A convention that needs more than 100 lines is probably an architecture doc or a tutorial — write it as one of those instead.
|
|
69
|
+
|
|
70
|
+
## Citation rules
|
|
71
|
+
|
|
72
|
+
- `## Example` and `## Counter-example` MUST cite real code (or be marked synthetic explicitly)
|
|
73
|
+
- Code references use `file:line@<sha7>` or `symbol` form
|
|
74
|
+
- LINT auto-fills missing `@<sha7>` on first save
|
|
75
|
+
|
|
76
|
+
## Skeleton
|
|
77
|
+
|
|
78
|
+
```markdown
|
|
79
|
+
---
|
|
80
|
+
schema_id: convention
|
|
81
|
+
schema_version: 1
|
|
82
|
+
scope: folder:src/routes
|
|
83
|
+
date: 2026-05-10
|
|
84
|
+
status: active
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## The convention
|
|
88
|
+
|
|
89
|
+
Route handlers live in `src/routes/{resource}.ts` and export a `{resource}Router` constant. Each handler function is exported separately for testing; the router is the wiring layer that mounts them.
|
|
90
|
+
|
|
91
|
+
## Rationale
|
|
92
|
+
|
|
93
|
+
Separates handler logic (testable in isolation) from routing (testable as integration). See [aiwiki/decisions/0017-handler-router-split.md](aiwiki/decisions/0017-handler-router-split.md).
|
|
94
|
+
|
|
95
|
+
## Example
|
|
96
|
+
|
|
97
|
+
[src/routes/users.ts:1-24@e5f6789](src/routes/users.ts) — `usersRouter` mounts three handlers (`listUsers`, `getUser`, `createUser`) that are individually exported.
|
|
98
|
+
|
|
99
|
+
## Counter-example
|
|
100
|
+
|
|
101
|
+
<!-- synthetic example; replace with real counter-example before publishing -->
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// DO NOT do this — handler logic and routing collapsed into a single anonymous function
|
|
105
|
+
app.get('/users', async (req, res) => {
|
|
106
|
+
const users = await db.users.findAll();
|
|
107
|
+
res.json(users);
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The closure can't be unit-tested without spinning up the router; the route registration and the handler are entangled.
|
|
112
|
+
```
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_id: decision
|
|
3
|
+
schema_version: 1
|
|
4
|
+
applies_to: aiwiki/decisions/**/*.md
|
|
5
|
+
filename_pattern: "{nnnn}-{slug}.md"
|
|
6
|
+
hard_cap_lines: 400
|
|
7
|
+
soft_target_lines: [100, 200]
|
|
8
|
+
required_frontmatter:
|
|
9
|
+
schema_id: { type: string, equals: decision }
|
|
10
|
+
schema_version: { type: integer }
|
|
11
|
+
status: { type: enum, values: [proposed, accepted, superseded, deprecated] }
|
|
12
|
+
date: { type: date }
|
|
13
|
+
supersedes: { type: string, optional: true }
|
|
14
|
+
superseded_by: { type: string, optional: true }
|
|
15
|
+
required_sections:
|
|
16
|
+
- "## Context"
|
|
17
|
+
- "## Decision"
|
|
18
|
+
- "## Consequences"
|
|
19
|
+
- "## Review"
|
|
20
|
+
optional_sections:
|
|
21
|
+
- "## Alternatives"
|
|
22
|
+
section_order: strict
|
|
23
|
+
citation_rule: required-where-claims-about-code
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# Schema: decision (ADR)
|
|
27
|
+
|
|
28
|
+
## Purpose
|
|
29
|
+
|
|
30
|
+
An ADR records a decision that is hard to reverse — architectural choice, public-surface naming, security/data tradeoff, schema design, cross-slice contract. The page answers "why was this chosen?" so future sessions don't relitigate it.
|
|
31
|
+
|
|
32
|
+
## When to write one
|
|
33
|
+
|
|
34
|
+
Write an ADR for any decision that's hard to reverse. Specifically:
|
|
35
|
+
|
|
36
|
+
- Architectural choice (system shape, data flow, technology selection)
|
|
37
|
+
- Public-surface naming (APIs, schemas, file paths users will import)
|
|
38
|
+
- Security or data-handling tradeoff
|
|
39
|
+
- Schema design (DB, API, file format) — anything other code will depend on
|
|
40
|
+
- Cross-module contract — what one module exposes that other modules depend on
|
|
41
|
+
- Any other irreversible design choice that survives prototype iteration into production
|
|
42
|
+
|
|
43
|
+
Do NOT write an ADR for: variable naming, inline-vs-extract refactors, choosing between known-good libraries with no real tradeoff. Agent judgment is sufficient there.
|
|
44
|
+
|
|
45
|
+
## File location and naming
|
|
46
|
+
|
|
47
|
+
- Path: `aiwiki/decisions/{nnnn}-{slug}.md`
|
|
48
|
+
- Numbering: zero-padded sequential, project-wide (e.g. `0042-token-storage.md`)
|
|
49
|
+
- Slug: kebab-case, ≤6 words, descriptive
|
|
50
|
+
|
|
51
|
+
## Required frontmatter
|
|
52
|
+
|
|
53
|
+
| Field | Type | Notes |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `schema_id` | string | Must equal `decision` |
|
|
56
|
+
| `schema_version` | integer | Bumped only when schema itself changes |
|
|
57
|
+
| `status` | enum | `proposed` / `accepted` / `superseded` / `deprecated` |
|
|
58
|
+
| `date` | ISO date | When the decision was accepted (not when drafted) |
|
|
59
|
+
| `supersedes` | string (optional) | Path to ADR this replaces |
|
|
60
|
+
| `superseded_by` | string (optional) | Set when this ADR is itself replaced |
|
|
61
|
+
|
|
62
|
+
## Required sections
|
|
63
|
+
|
|
64
|
+
| Section | Purpose | Citation requirement |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `## Context` | What's the situation that demands a decision | Cite code if context is grounded in specific code |
|
|
67
|
+
| `## Decision` | What we chose (one paragraph, imperative mood) | Cite the prototype file or convention this comes from |
|
|
68
|
+
| `## Alternatives` (optional) | What we rejected and why | Omit if the alternatives are obvious |
|
|
69
|
+
| `## Consequences` | What changes downstream | Cite affected files where known |
|
|
70
|
+
| `## Review` | `review:` block — reviewers, raised objections, how each was resolved, final verdict | — |
|
|
71
|
+
|
|
72
|
+
The `## Review` block is required for `accepted` status. Format:
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
review:
|
|
76
|
+
reviewers: [critic, codex]
|
|
77
|
+
claims: |
|
|
78
|
+
<what the decision claims to be true/best>
|
|
79
|
+
objections:
|
|
80
|
+
- reviewer: critic
|
|
81
|
+
objection: "..."
|
|
82
|
+
resolution: "..."
|
|
83
|
+
verdict: approved | escalate | reject
|
|
84
|
+
reviewed_at: 2026-05-10T11:42:00Z
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Line caps
|
|
88
|
+
|
|
89
|
+
- Hard cap: 400 lines (LINT fails above)
|
|
90
|
+
- Soft target: 100-200 lines
|
|
91
|
+
|
|
92
|
+
## Citation rules
|
|
93
|
+
|
|
94
|
+
- Code references use `file:line@<sha7>` (e.g. `src/auth.ts:42@a3f2bc1`) or `symbol` form (e.g. `src/auth.ts#login`)
|
|
95
|
+
- LINT auto-fills missing `@<sha7>` on first save
|
|
96
|
+
- Stale citations (hash mismatch) fail LINT — resolve by updating the citation, removing the claim, or annotating `// ack-stale: <reason>`
|
|
97
|
+
|
|
98
|
+
## Skeleton
|
|
99
|
+
|
|
100
|
+
```markdown
|
|
101
|
+
---
|
|
102
|
+
schema_id: decision
|
|
103
|
+
schema_version: 1
|
|
104
|
+
status: accepted
|
|
105
|
+
date: 2026-05-10
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Context
|
|
109
|
+
|
|
110
|
+
The cache layer needs durable storage. Read-heavy workload (~95% reads); single writer pattern; ≤1M entries projected.
|
|
111
|
+
|
|
112
|
+
## Decision
|
|
113
|
+
|
|
114
|
+
Use SQLite for the cache layer.
|
|
115
|
+
|
|
116
|
+
## Alternatives
|
|
117
|
+
|
|
118
|
+
- **Postgres**: rejected — overkill for single-writer cache; operational burden of a separate service.
|
|
119
|
+
- **In-memory only**: rejected — process restart loses cache; eviction strategy adds complexity not warranted at projected size.
|
|
120
|
+
|
|
121
|
+
## Consequences
|
|
122
|
+
|
|
123
|
+
- One additional dependency (`better-sqlite3`)
|
|
124
|
+
- WAL mode enabled for concurrent reader safety
|
|
125
|
+
- Migration path documented at [src/cache/migrations/README.md](src/cache/migrations/README.md)
|
|
126
|
+
|
|
127
|
+
## Review
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
review:
|
|
131
|
+
reviewers: [critic, codex]
|
|
132
|
+
claims: |
|
|
133
|
+
SQLite is sufficient for the cache layer at projected scale and read pattern.
|
|
134
|
+
objections:
|
|
135
|
+
- reviewer: critic
|
|
136
|
+
objection: "Concurrent write throughput at scale"
|
|
137
|
+
resolution: "Cache is read-heavy; single-writer pattern documented; WAL mode handles reader concurrency"
|
|
138
|
+
- reviewer: codex
|
|
139
|
+
objection: "No atomic multi-row updates"
|
|
140
|
+
resolution: "Acknowledged; not needed for cache semantics (per-key invalidation only)"
|
|
141
|
+
verdict: approved
|
|
142
|
+
reviewed_at: 2026-05-10T11:42:00Z
|
|
143
|
+
```
|
|
144
|
+
```
|