mikoshi-construct 0.1.3 → 0.3.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/README.md +34 -5
- package/dist/cli.js +2060 -525
- package/package.json +15 -10
- package/templates/ai/claude/_claude/agents/architect.md +7 -11
- package/templates/ai/claude/_claude/agents/harness.md +7 -11
- package/templates/ai/claude/_claude/agents/implementer.md +6 -10
- package/templates/ai/claude/_claude/commands/plan.md +2 -0
- package/templates/ai/claude/_claude/skills/implement/SKILL.md +43 -8
- package/templates/ai/claude/scripts/construct/implement.workflow.mjs +103 -30
- package/templates/ai/cursor/_cursor/rules/construct.mdc +1 -1
- package/templates/ai/shared/_claude/commands/construct-discover.md +25 -4
- package/templates/base/architecture/decisions/README.md +26 -0
- package/templates/base/architecture/principles.md +3 -1
- package/templates/presets/monorepo/baseline/eslint.config.mjs.eta +22 -6
- package/templates/presets/monorepo/sample/scripts/tests/lint/syntax-policy.test.ts.eta +99 -25
- package/templates/presets/node-backend/baseline/eslint.config.mjs +11 -3
- package/templates/presets/node-backend/sample/scripts/tests/lint/syntax-policy.test.ts +73 -18
- package/templates/presets/node-frontend/baseline/eslint.config.mjs +12 -3
- package/templates/presets/node-frontend/sample/scripts/tests/lint/syntax-policy.test.ts +78 -0
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikoshi-construct",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "Bootstrap for AI-native software projects. Start with a proven engineering workflow instead of an empty repository.",
|
|
6
6
|
"author": "Eli Tabrisov",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"homepage": "https://github.
|
|
8
|
+
"homepage": "https://e1i.github.io/mikoshi-construct/",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/E1i/mikoshi-construct.git"
|
|
@@ -49,23 +49,28 @@
|
|
|
49
49
|
"tsup": "^8.5.1",
|
|
50
50
|
"tsx": "^4.23.13",
|
|
51
51
|
"typescript": "^5.9.3",
|
|
52
|
+
"vitepress": "2.0.0-alpha.20",
|
|
52
53
|
"vitest": "^5.0.0",
|
|
53
54
|
"yaml": "^2.9.1"
|
|
54
55
|
},
|
|
55
56
|
"scripts": {
|
|
56
57
|
"build": "tsup",
|
|
58
|
+
"changeset": "changeset",
|
|
59
|
+
"ci": "pnpm run quality",
|
|
60
|
+
"composition:check": "tsx scripts/composition/check.ts",
|
|
61
|
+
"composition:render": "tsx scripts/composition/sync-docs.ts",
|
|
57
62
|
"dev": "tsx src/cli.ts",
|
|
63
|
+
"docs:build": "vitepress build docs",
|
|
64
|
+
"docs:dev": "vitepress dev docs",
|
|
65
|
+
"docs:preview": "vitepress preview docs",
|
|
58
66
|
"lint": "eslint .",
|
|
59
67
|
"lint:fix": "eslint --fix .",
|
|
60
|
-
"
|
|
68
|
+
"privacy:check": "tsx scripts/privacy/check.ts",
|
|
69
|
+
"quality": "pnpm composition:check && pnpm privacy:check && pnpm lint && pnpm typecheck && pnpm test",
|
|
70
|
+
"release": "pnpm build && changeset publish",
|
|
61
71
|
"test": "vitest run",
|
|
62
72
|
"test:watch": "vitest",
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"composition:check": "tsx scripts/composition/check.ts",
|
|
66
|
-
"ci": "pnpm run quality",
|
|
67
|
-
"changeset": "changeset",
|
|
68
|
-
"version-packages": "changeset version",
|
|
69
|
-
"release": "pnpm build && changeset publish"
|
|
73
|
+
"typecheck": "tsc --noEmit",
|
|
74
|
+
"version-packages": "changeset version"
|
|
70
75
|
}
|
|
71
76
|
}
|
|
@@ -24,15 +24,11 @@ Work through, in this order, and write nothing down until you have:
|
|
|
24
24
|
5. The explicit constraints the implementer must respect, and the acceptance criteria that make
|
|
25
25
|
the task statable.
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Return these fields; the runtime validates the shape against the schema it gives you.
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"acceptance": ["one verifiable criterion per entry"],
|
|
36
|
-
"files": ["files to create or change"]
|
|
37
|
-
}
|
|
38
|
-
```
|
|
29
|
+
- `decision` — the design in a few sentences, including what stays unchanged and why.
|
|
30
|
+
- `contractChanges` — operations or schemas to add or change in the API contract, empty when none.
|
|
31
|
+
- `compositionChanges` — composition model nodes or edges to change, empty when none.
|
|
32
|
+
- `constraints` — one constraint per entry, each one the implementer must respect.
|
|
33
|
+
- `acceptance` — one verifiable criterion per entry.
|
|
34
|
+
- `files` — the files to create or change.
|
|
@@ -26,15 +26,11 @@ When git is usable, also inspect `git diff` (staged and unstaged) for:
|
|
|
26
26
|
Report the security leg separately from the rest when a failure comes from a security lint rule,
|
|
27
27
|
a contract security test or the secret scan, so the reader sees the invariant, not just the tool.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Return these fields; the runtime validates the shape against the schema it gives you.
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"testsWeakened": false,
|
|
38
|
-
"contractChanged": false
|
|
39
|
-
}
|
|
40
|
-
```
|
|
31
|
+
- `passed` — whether every harness command succeeded.
|
|
32
|
+
- `failureExcerpt` — the failing command and its last relevant lines, empty when passed.
|
|
33
|
+
- `securityFinding` — the invariant that failed, empty when none.
|
|
34
|
+
- `diffStat` — the output of `git diff --stat`, or why git could not be used.
|
|
35
|
+
- `testsWeakened` — whether a test was deleted, renamed away, skipped or narrowed.
|
|
36
|
+
- `contractChanged` — whether a file under `contracts/` changed.
|
|
@@ -27,14 +27,10 @@ Constraints at this effort level:
|
|
|
27
27
|
If the task is ambiguous about a contract, a boundary or which of two designs is meant, stop and
|
|
28
28
|
report `blocked` with one precise question. Do not pick one.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Return these fields; the runtime validates the shape against the schema it gives you.
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"harnessTail": "last lines of the harness output, empty when blocked",
|
|
38
|
-
"question": "the single question when blocked, otherwise empty"
|
|
39
|
-
}
|
|
40
|
-
```
|
|
32
|
+
- `status` — `done`, `failed` or `blocked`.
|
|
33
|
+
- `summary` — one or two sentences on what changed.
|
|
34
|
+
- `files` — the paths you changed.
|
|
35
|
+
- `harnessTail` — the last lines of the harness output, empty when blocked.
|
|
36
|
+
- `question` — the single question when blocked, otherwise empty.
|
|
@@ -12,6 +12,8 @@ Rules:
|
|
|
12
12
|
- Two to six tasks. Each task is independently verifiable by the harness and leaves the tree green.
|
|
13
13
|
- Each task has two to four acceptance criteria that a harness run or a test can confirm. "Works" is
|
|
14
14
|
not a criterion; "GET /v1/things returns 200 with the `Thing` schema and the contract test passes" is.
|
|
15
|
+
- A criterion is verified by what the task changes itself. If satisfying it needs an action outside the
|
|
16
|
+
task, it belongs to that task, not this one.
|
|
15
17
|
- Order tasks so the contract and composition changes come first, then implementation, then anything
|
|
16
18
|
that consumes the new behaviour.
|
|
17
19
|
- Classify each task `low`, `medium` or `high` with the rules in `architecture/principles.md`
|
|
@@ -23,17 +23,52 @@ repository's CLAUDE.md and `construct.json`.
|
|
|
23
23
|
a JSON object:
|
|
24
24
|
`{ "task": ..., "acceptance": [...], "effort": "low|medium|high", "harness": { "command": ..., "extra": [...] } }`
|
|
25
25
|
where `harness.command` comes from `construct.json` and `harness.extra` lists any area-specific
|
|
26
|
-
commands CLAUDE.md names for the files the task touches (usually empty).
|
|
27
|
-
|
|
26
|
+
commands CLAUDE.md names for the files the task touches (usually empty). `retryLimit` is optional
|
|
27
|
+
and defaults to `0`: a rejected response is not re-asked, and the run stops with the validator's
|
|
28
|
+
error so a person reads it. Each retry is a whole new agent call that repeats the agent's
|
|
29
|
+
exploration from scratch — measured at roughly three million billable tokens for an architect —
|
|
30
|
+
and it cannot fix a contradiction in the task, because the agent may not change the task. Raise it
|
|
31
|
+
only when a rejected response is expected to be a transient shape error rather than a bad brief.
|
|
32
|
+
The user's `/implement` invocation is the opt-in the tool requires. Note the run identifier the
|
|
33
|
+
Workflow tool reports when it launches the run and again when it completes; step 4 records it.
|
|
34
|
+
The design step runs inside the ladder, not before it, and its outcome is one of the `attempts`
|
|
35
|
+
like any other. The statuses a run can return are:
|
|
36
|
+
- `done` — a rung passed the harness and every design step the run took completed.
|
|
37
|
+
- `degraded` — a rung passed the harness, but a design step was rejected by the schema and the
|
|
38
|
+
run continued without it. The result's `effort` is the class that actually executed.
|
|
39
|
+
- `design incomplete` — a high-effort run whose architect was rejected by the schema. No
|
|
40
|
+
implementer ran without a spec; the result carries the validator's text in `validationError`.
|
|
41
|
+
- `failed` — every rung ran and the harness stayed red; `lastFailure` carries the excerpt.
|
|
42
|
+
- `blocked` — the last rung stopped on a question; `question` carries it verbatim.
|
|
28
43
|
4. Record the run: append one JSON line to `.construct/runs.jsonl` (create the directory if needed)
|
|
29
|
-
with
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
with exactly these fields and no others:
|
|
45
|
+
- `run` — the Workflow run identifier from step 3. It is the key `construct cost` joins the entry
|
|
46
|
+
to the runtime's session data on. Never invent one: an entry without it is reported as
|
|
47
|
+
unjoinable, which is the truth about it.
|
|
48
|
+
- `at` — the ISO timestamp.
|
|
49
|
+
- `task` — the task text, first 120 characters.
|
|
50
|
+
- `effort` — the class the run performed: the result's `effort` when it carries one, and only
|
|
51
|
+
then the class you chose in step 1. A run whose design step did not complete is never written
|
|
52
|
+
down as `high`; the result has already degraded it.
|
|
53
|
+
- `status` — the result's status verbatim, one of the five in step 3.
|
|
54
|
+
- `rung` — the effort of the rung that finished: `effort` from the result when it carries one,
|
|
55
|
+
otherwise the `effort` of the last entry in `attempts`.
|
|
56
|
+
- `attempts` — the result's `attempts` array verbatim; each entry carries its `rung`, `effort`,
|
|
57
|
+
`outcome` and the `reason` that separates an invalid response shape from a red harness, from a
|
|
58
|
+
blocked report and from a design the schema rejected.
|
|
59
|
+
- `agents`, `tokens`, `toolUses`, `seconds` — the Workflow tool's own accounting for the run,
|
|
60
|
+
exactly as it reported it. Write `"unknown"` for a token figure it did not report, never `0`.
|
|
61
|
+
The ledger carries counts and reasons only — never a prompt, a response or any other message
|
|
62
|
+
content. This log is what tunes the ladder later; workflow scripts have no filesystem access, so
|
|
63
|
+
it is written here, not by the script. Nothing enforces this step: the ledger is L0, and
|
|
64
|
+
`construct cost` reconciles it against the runtime instead of trusting it. `.construct/` is
|
|
65
|
+
gitignored.
|
|
34
66
|
5. Relay the result: status, the effort rung that succeeded and how many attempts it took, the
|
|
35
67
|
files changed, and the harness tail. When the status is `blocked`, put the architect's or
|
|
36
|
-
implementer's question to the user verbatim. When `failed`, give the last failure excerpt.
|
|
68
|
+
implementer's question to the user verbatim. When `failed`, give the last failure excerpt. When
|
|
69
|
+
`design incomplete`, say that the design step did not complete and give `validationError` as the
|
|
70
|
+
runtime reported it; when `degraded`, say which design step was rejected and that the reported
|
|
71
|
+
class is the one that executed, not the one that was requested.
|
|
37
72
|
Unless `construct.json` sets `report.usage` to `false`, end with one usage line for this run,
|
|
38
73
|
from the Workflow tool's own accounting: agents, subagent tokens, tool uses, wall time — so the
|
|
39
74
|
cost of the rung that succeeded is on record next to the result. When `construct` is on the PATH,
|
|
@@ -2,7 +2,7 @@ export const meta = {
|
|
|
2
2
|
name: 'implement',
|
|
3
3
|
description: 'Implement a task at low effort, verify with the harness, escalate on repeated failure or ambiguity',
|
|
4
4
|
phases: [
|
|
5
|
-
{ title: 'Design', detail: 'architect,
|
|
5
|
+
{ title: 'Design', detail: 'architect inside the run, for high effort before the first rung and after a blocked or failed attempt' },
|
|
6
6
|
{ title: 'Implement', detail: 'implementer at the current rung' },
|
|
7
7
|
{ title: 'Verify', detail: 'harness against the working tree' },
|
|
8
8
|
],
|
|
@@ -52,10 +52,48 @@ const SPEC = {
|
|
|
52
52
|
},
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
const DEFAULT_RETRY_LIMIT = 0
|
|
56
|
+
const DESIGN_EFFORT = 'xhigh'
|
|
57
|
+
const EFFORT_WITHOUT_DESIGN = { high: 'medium', xhigh: 'medium' }
|
|
58
|
+
|
|
55
59
|
const task = args.task
|
|
56
60
|
const acceptance = args.acceptance ?? []
|
|
57
61
|
const harness = { command: 'pnpm run quality', extra: [], ...(args.harness ?? {}) }
|
|
58
62
|
const rungs = LADDERS[args.effort] ?? LADDERS.low
|
|
63
|
+
const retryLimit = Number.isInteger(args.retryLimit) && args.retryLimit >= 0 ? args.retryLimit : DEFAULT_RETRY_LIMIT
|
|
64
|
+
|
|
65
|
+
let lastValidationError = null
|
|
66
|
+
|
|
67
|
+
function retryPrompt(prompt, validationError) {
|
|
68
|
+
return `${prompt}\n\nThe previous response did not match the shape the runtime validates. The validator reported:\n${validationError}\n\nReturn the same fields again with that corrected.`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function askOnce(prompt, options) {
|
|
72
|
+
try {
|
|
73
|
+
const value = await agent(prompt, options)
|
|
74
|
+
return value == null
|
|
75
|
+
? { value: null, validationError: 'the agent returned no object the schema could validate' }
|
|
76
|
+
: { value, validationError: null }
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
return { value: null, validationError: String(error?.message ?? error) }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function ask(prompt, options) {
|
|
84
|
+
let validationError = null
|
|
85
|
+
for (let attempt = 0; attempt <= retryLimit; attempt++) {
|
|
86
|
+
const answer = await askOnce(validationError == null ? prompt : retryPrompt(prompt, validationError), options)
|
|
87
|
+
if (answer.value != null) {
|
|
88
|
+
lastValidationError = null
|
|
89
|
+
return answer.value
|
|
90
|
+
}
|
|
91
|
+
validationError = answer.validationError
|
|
92
|
+
log(`${options.label}: response rejected by the schema — ${validationError}`)
|
|
93
|
+
}
|
|
94
|
+
lastValidationError = validationError
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
59
97
|
|
|
60
98
|
function harnessPrompt() {
|
|
61
99
|
return [
|
|
@@ -89,25 +127,61 @@ function implementerPrompt(spec, feedback) {
|
|
|
89
127
|
|
|
90
128
|
let spec = null
|
|
91
129
|
let feedback = null
|
|
130
|
+
let designComplete = false
|
|
131
|
+
let designFailed = false
|
|
132
|
+
let designError = ''
|
|
92
133
|
const attempts = []
|
|
93
134
|
|
|
135
|
+
function performedEffort(effort) {
|
|
136
|
+
return designComplete ? effort : (EFFORT_WITHOUT_DESIGN[effort] ?? effort)
|
|
137
|
+
}
|
|
94
138
|
|
|
95
|
-
|
|
139
|
+
async function design(rung, reason, label) {
|
|
96
140
|
phase('Design')
|
|
97
|
-
|
|
141
|
+
const result = await ask(architectPrompt(reason), {
|
|
98
142
|
agentType: 'architect',
|
|
99
|
-
effort:
|
|
143
|
+
effort: DESIGN_EFFORT,
|
|
100
144
|
phase: 'Design',
|
|
101
|
-
label
|
|
145
|
+
label,
|
|
102
146
|
schema: SPEC,
|
|
103
147
|
})
|
|
148
|
+
if (result == null) {
|
|
149
|
+
designComplete = false
|
|
150
|
+
designFailed = true
|
|
151
|
+
designError = lastValidationError ?? ''
|
|
152
|
+
attempts.push({ rung, effort: DESIGN_EFFORT, outcome: 'design schema invalid', reason: designError })
|
|
153
|
+
log(`${label}: the design step did not complete`)
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
156
|
+
spec = result
|
|
157
|
+
designComplete = true
|
|
158
|
+
attempts.push({ rung, effort: DESIGN_EFFORT, outcome: 'designed', reason: '' })
|
|
159
|
+
return true
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function designIncomplete(effort, question) {
|
|
163
|
+
return {
|
|
164
|
+
status: 'design incomplete',
|
|
165
|
+
effort: performedEffort(effort),
|
|
166
|
+
attempts,
|
|
167
|
+
validationError: designError,
|
|
168
|
+
question: question ?? '',
|
|
169
|
+
lastFailure: feedback ?? '',
|
|
170
|
+
}
|
|
104
171
|
}
|
|
105
172
|
|
|
106
173
|
for (const [index, effort] of rungs.entries()) {
|
|
107
174
|
const rung = index + 1
|
|
175
|
+
|
|
176
|
+
if (rung === 1 && args.effort === 'high') {
|
|
177
|
+
const designed = await design(rung, 'The task is classified as high effort; design it before any implementation.', 'design')
|
|
178
|
+
if (!designed)
|
|
179
|
+
return designIncomplete(effort)
|
|
180
|
+
}
|
|
181
|
+
|
|
108
182
|
phase('Implement')
|
|
109
183
|
log(`rung ${rung}/${rungs.length} @ ${effort}: implementing`)
|
|
110
|
-
const report = await
|
|
184
|
+
const report = await ask(implementerPrompt(spec, feedback), {
|
|
111
185
|
agentType: 'implementer',
|
|
112
186
|
effort,
|
|
113
187
|
phase: 'Implement',
|
|
@@ -115,29 +189,25 @@ for (const [index, effort] of rungs.entries()) {
|
|
|
115
189
|
schema: REPORT,
|
|
116
190
|
})
|
|
117
191
|
if (report == null) {
|
|
118
|
-
attempts.push({ rung, effort, outcome: '
|
|
192
|
+
attempts.push({ rung, effort, outcome: 'schema invalid', reason: lastValidationError })
|
|
119
193
|
continue
|
|
120
194
|
}
|
|
121
195
|
|
|
122
196
|
if (report.status === 'blocked') {
|
|
123
|
-
attempts.push({ rung, effort, outcome: 'blocked', question: report.question })
|
|
197
|
+
attempts.push({ rung, effort, outcome: 'blocked', reason: report.question, question: report.question })
|
|
124
198
|
if (rung === rungs.length)
|
|
125
199
|
return { status: 'blocked', question: report.question, attempts }
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
effort
|
|
129
|
-
phase: 'Design',
|
|
130
|
-
label: `design after blocked ${rung}`,
|
|
131
|
-
schema: SPEC,
|
|
132
|
-
})
|
|
200
|
+
const designed = await design(rung, `The implementer stopped on this question:\n${report.question}`, `design after blocked ${rung}`)
|
|
201
|
+
if (!designed && args.effort === 'high')
|
|
202
|
+
return designIncomplete(effort, report.question)
|
|
133
203
|
feedback = null
|
|
134
|
-
log(`rung ${rung}/${rungs.length} @ ${effort}: blocked, architect answered`)
|
|
204
|
+
log(`rung ${rung}/${rungs.length} @ ${effort}: blocked, architect ${designed ? 'answered' : 'did not answer'}`)
|
|
135
205
|
continue
|
|
136
206
|
}
|
|
137
207
|
|
|
138
208
|
phase('Verify')
|
|
139
209
|
log(`rung ${rung}/${rungs.length} @ ${effort}: running ${harness.command}`)
|
|
140
|
-
const verdict = await
|
|
210
|
+
const verdict = await ask(harnessPrompt(), {
|
|
141
211
|
agentType: 'harness',
|
|
142
212
|
effort: 'low',
|
|
143
213
|
phase: 'Verify',
|
|
@@ -145,13 +215,21 @@ for (const [index, effort] of rungs.entries()) {
|
|
|
145
215
|
schema: VERDICT,
|
|
146
216
|
})
|
|
147
217
|
const passed = verdict?.passed === true && verdict.testsWeakened === false
|
|
148
|
-
attempts.push(
|
|
218
|
+
attempts.push(verdict == null
|
|
219
|
+
? { rung, effort, outcome: 'schema invalid', reason: lastValidationError, securityFinding: '' }
|
|
220
|
+
: {
|
|
221
|
+
rung,
|
|
222
|
+
effort,
|
|
223
|
+
outcome: passed ? 'passed' : 'harness failed',
|
|
224
|
+
reason: passed ? '' : (verdict.testsWeakened ? 'a test was deleted, skipped or narrowed' : verdict.failureExcerpt),
|
|
225
|
+
securityFinding: verdict.securityFinding ?? '',
|
|
226
|
+
})
|
|
149
227
|
log(`rung ${rung} @ ${effort}: ${passed ? 'harness passed' : 'harness failed'}`)
|
|
150
228
|
|
|
151
229
|
if (passed) {
|
|
152
230
|
return {
|
|
153
|
-
status: 'done',
|
|
154
|
-
effort,
|
|
231
|
+
status: designFailed && !designComplete ? 'degraded' : 'done',
|
|
232
|
+
effort: performedEffort(effort),
|
|
155
233
|
attempts,
|
|
156
234
|
files: report.files,
|
|
157
235
|
summary: report.summary,
|
|
@@ -162,7 +240,7 @@ for (const [index, effort] of rungs.entries()) {
|
|
|
162
240
|
}
|
|
163
241
|
|
|
164
242
|
feedback = verdict == null
|
|
165
|
-
?
|
|
243
|
+
? `The harness produced no verdict the schema could validate: ${lastValidationError}`
|
|
166
244
|
: verdict.testsWeakened
|
|
167
245
|
? `A test was deleted, skipped or narrowed. Restore it and make the implementation pass it.\n${verdict.failureExcerpt}`
|
|
168
246
|
: verdict.failureExcerpt
|
|
@@ -170,16 +248,11 @@ for (const [index, effort] of rungs.entries()) {
|
|
|
170
248
|
feedback = `Security invariant failed: ${verdict.securityFinding}\n${feedback}`
|
|
171
249
|
|
|
172
250
|
if (rung === rungs.length - 1) {
|
|
173
|
-
phase('Design')
|
|
174
251
|
log(`rung ${rung}/${rungs.length} failed twice: architect redesigns before the last rung`)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
effort
|
|
178
|
-
phase: 'Design',
|
|
179
|
-
label: 'design before last rung',
|
|
180
|
-
schema: SPEC,
|
|
181
|
-
})
|
|
252
|
+
const designed = await design(rung, `Two rungs have failed the harness. Latest failure:\n${feedback}\n\nDecide whether the approach, the contract or the boundary is wrong before the last attempt.`, 'design before last rung')
|
|
253
|
+
if (!designed && args.effort === 'high')
|
|
254
|
+
return designIncomplete(effort)
|
|
182
255
|
}
|
|
183
256
|
}
|
|
184
257
|
|
|
185
|
-
return { status: 'failed', attempts, lastFailure: feedback }
|
|
258
|
+
return { status: 'failed', attempts, lastFailure: feedback, effort: performedEffort(rungs[rungs.length - 1]) }
|
|
@@ -11,7 +11,7 @@ areas, defects vs accepted variance), `architecture/security-invariants.md`.
|
|
|
11
11
|
The harness command is in `construct.json` (`harness.command`). A change is done only when it passes.
|
|
12
12
|
API changes start from the contract file named in `construct.json` (`contracts.path`); regenerate the
|
|
13
13
|
types with `pnpm contracts:types` before implementing. Composition models in the directory
|
|
14
|
-
`construct.json` names as `discovery.composition` are the source of the diagrams; edit the model and run
|
|
14
|
+
`construct.json` names as `discovery.markers.composition.file` are the source of the diagrams; edit the model and run
|
|
15
15
|
`pnpm composition:render`, never the rendered block.
|
|
16
16
|
|
|
17
17
|
Discovery: when asked to "run construct discovery", follow the `construct-discover` rule in this
|
|
@@ -9,8 +9,8 @@ not verify by reading code; where the codebase is inconsistent, record an open q
|
|
|
9
9
|
inventing a rule.
|
|
10
10
|
|
|
11
11
|
Scope: `$ARGUMENTS` (empty means every marker). The markers, and the file each one lives in, are
|
|
12
|
-
listed under `discovery` in `construct.json` (the text markers in `AGENTS.md`, the composition
|
|
13
|
-
in the directory `discovery.composition` names). Every marker is a block between
|
|
12
|
+
listed under `discovery.markers` in `construct.json` (the text markers in `AGENTS.md`, the composition
|
|
13
|
+
models in the directory `discovery.markers.composition.file` names). Every marker is a block between
|
|
14
14
|
`<!-- construct:discover:<name> -->` and `<!-- /construct:discover:<name> -->`; replace the placeholder
|
|
15
15
|
line inside the block and nothing outside it. `construct doctor` reports any marker still holding the
|
|
16
16
|
placeholder.
|
|
@@ -37,7 +37,9 @@ Work in this order:
|
|
|
37
37
|
2. **Inventory.** Read `construct.json`, `package.json`, the directory tree two levels deep, the entry
|
|
38
38
|
points (servers, app factories, `main.ts`, CLI scripts, workers), the API contract if there is
|
|
39
39
|
one, and every `*.config.ts` / `config.ts`. Note the package manager, runtime, database and clients, CI, deployment
|
|
40
|
-
and existing conventions. Do not write yet.
|
|
40
|
+
and existing conventions. Do not write yet. Record the commit this run starts from: set
|
|
41
|
+
`discovery.baseSha` in `construct.json` to the output of `git rev-parse HEAD`, or `null` where the
|
|
42
|
+
repository has no commit yet.
|
|
41
43
|
3. **`product`** (AGENTS.md): what the system does, in one paragraph, and the one flow where a
|
|
42
44
|
defect costs the most (money, identity, data). If the repository is empty apart from the baseline,
|
|
43
45
|
say so in one line.
|
|
@@ -54,7 +56,7 @@ Work in this order:
|
|
|
54
56
|
8. **`high-effort-areas`** (AGENTS.md): the paths where a wrong low-effort guess is expensive —
|
|
55
57
|
attribution, authentication, money, schema, anything a shipped client depends on. This list is what
|
|
56
58
|
`/implement` uses to classify a task as `high`.
|
|
57
|
-
9. **`composition`** (`<discovery.composition>/*.yaml` from `construct.json`): one model per real flow the code has today
|
|
59
|
+
9. **`composition`** (`<discovery.markers.composition.file>/*.yaml` from `construct.json`): one model per real flow the code has today
|
|
58
60
|
(the HTTP app, a worker, a sync, a CLI, the browser bootstrap) — small, one per flow, every `path`
|
|
59
61
|
must exist. A baseline model, when the construct shipped one, is updated, not duplicated; a
|
|
60
62
|
repository that had code before the construct starts with no model and needs at least one for its
|
|
@@ -71,5 +73,24 @@ Work in this order:
|
|
|
71
73
|
`npx mikoshi-construct doctor` when the CLI is not installed; it names every marker that still
|
|
72
74
|
holds the placeholder.
|
|
73
75
|
|
|
76
|
+
13. **Record what you wrote, in the manifest and nowhere else.** Provenance belongs in
|
|
77
|
+
`construct.json`; never add a byline, an authorship note or a hash to a marker body. Set
|
|
78
|
+
`discovery.filledAt` to the time you finished, and for each marker you filled set
|
|
79
|
+
`discovery.markers.<name>` to `{"file": "<the file it lives in>", "authoredBy": "construct",
|
|
80
|
+
"sha": "<sha256 of the body you wrote>"}`. A marker you did not fill keeps the entry it had.
|
|
81
|
+
The body is the text between `<!-- construct:discover:<name> -->` and
|
|
82
|
+
`<!-- /construct:discover:<name> -->` with leading and trailing whitespace stripped; for
|
|
83
|
+
`composition` it is every `*.yaml` in the directory, sorted by name, each as its filename, a
|
|
84
|
+
newline and its contents, joined by newlines. Compute it, never estimate it:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
node -e 'const{createHash}=require("node:crypto"),{readFileSync}=require("node:fs");const[f,m]=process.argv.slice(1);const d=readFileSync(f,"utf8"),o=`<!-- construct:discover:${m} -->`,c=`<!-- /construct:discover:${m} -->`;console.log(createHash("sha256").update(d.slice(d.indexOf(o)+o.length,d.indexOf(c)).trim()).digest("hex"))' AGENTS.md product
|
|
88
|
+
node -e 'const{createHash}=require("node:crypto"),{readFileSync,readdirSync}=require("node:fs"),p=require("node:path");const d=process.argv[1],b=readdirSync(d).filter(n=>n.endsWith(".yaml")).sort().map(n=>`${n}\n${readFileSync(p.join(d,n),"utf8")}`).join("\n");console.log(createHash("sha256").update(b).digest("hex"))' architecture/composition
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
This is what keeps a statement the tool wrote from later reading as one the repository stands
|
|
92
|
+
behind. The moment the owner edits a marker its body stops matching the recorded sha and
|
|
93
|
+
`construct doctor` reads it as theirs — the edit is the evidence, and there is no command to run.
|
|
94
|
+
|
|
74
95
|
Report: which markers you filled, which you left as open questions and why, and the harness result.
|
|
75
96
|
Do not commit.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Decisions
|
|
2
|
+
|
|
3
|
+
One file per decision that shapes what this project is allowed to claim. A decision lands here when
|
|
4
|
+
reversing it would cost more than making it did, or when a later reader would otherwise re-open it
|
|
5
|
+
from scratch. Read this directory before re-opening a question it already answers, and add a record
|
|
6
|
+
rather than restating a decision in a plan or a commit message.
|
|
7
|
+
|
|
8
|
+
Each record carries four sections: the context it was taken in, the decision itself, the
|
|
9
|
+
consequences it accepts, and how it is enforced.
|
|
10
|
+
|
|
11
|
+
Enforcement is named on a scale, so the strength of a decision is visible rather than implied:
|
|
12
|
+
|
|
13
|
+
| Level | Enforced by |
|
|
14
|
+
|---|---|
|
|
15
|
+
| L0 | text only — nothing checks it |
|
|
16
|
+
| L1 | review — a human is the check |
|
|
17
|
+
| L2 | a local hook — bypassable with `--no-verify` |
|
|
18
|
+
| L3 | CI — it runs on every push |
|
|
19
|
+
| L4 | CI that blocks the merge |
|
|
20
|
+
|
|
21
|
+
A decision enforced only by review says so; that is the honest answer, not a gap to hide.
|
|
22
|
+
|
|
23
|
+
Records are named `NNNN-a-short-title.md` and numbered in the order they were taken.
|
|
24
|
+
|
|
25
|
+
| # | Decision | Enforced at |
|
|
26
|
+
|---|---|---|
|
|
@@ -68,7 +68,9 @@ isolated and manageable — and to treat accidental complexity as a defect.
|
|
|
68
68
|
reconsider the decomposition, coupling or abstraction instead of pushing through.
|
|
69
69
|
|
|
70
70
|
The checklists that turn these principles into questions to answer before a change are in
|
|
71
|
-
[checklists.md](checklists.md).
|
|
71
|
+
[checklists.md](checklists.md). A decision that shapes what this project may claim is recorded once in
|
|
72
|
+
[decisions/](decisions/README.md), with the level at which it is enforced, instead of being restated in
|
|
73
|
+
a plan or a commit message.
|
|
72
74
|
|
|
73
75
|
## Security
|
|
74
76
|
|
|
@@ -4,11 +4,27 @@ const WORKSPACE_PACKAGES = {{workspacePackages}}
|
|
|
4
4
|
|
|
5
5
|
const ALLOWED_WORKSPACE_IMPORTS = {{allowedWorkspaceImports}}
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const PROCESS_ENV_MEMBER = 'MemberExpression[object.name="process"][property.name="env"]'
|
|
8
|
+
const PROCESS_ENV_VIA_GLOBAL_THIS = 'MemberExpression[object.object.name="globalThis"][object.property.name="process"][property.name="env"]'
|
|
9
|
+
const PROCESS_ENV_DESTRUCTURED = 'VariableDeclarator[init.name="process"] > ObjectPattern > Property[key.name="env"]'
|
|
10
|
+
const PROCESS_ENV = `:matches(${PROCESS_ENV_MEMBER}, ${PROCESS_ENV_VIA_GLOBAL_THIS}, ${PROCESS_ENV_DESTRUCTURED})`
|
|
11
|
+
|
|
8
12
|
const PROCESS_MEMBER = 'MemberExpression[object.name="process"]'
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
13
|
+
const PROCESS_VIA_GLOBAL_THIS = 'MemberExpression[object.name="globalThis"]:matches([property.name="process"], [property.value="process"])'
|
|
14
|
+
const PROCESS_BINDING = 'VariableDeclarator[init.name="process"]'
|
|
15
|
+
const PROCESS = `:matches(${PROCESS_MEMBER}, ${PROCESS_VIA_GLOBAL_THIS}, ${PROCESS_BINDING})`
|
|
16
|
+
|
|
17
|
+
const RAW_REQUEST_MEMBER = 'MemberExpression[object.name="req"][property.name=/^(body|query|params)$/]'
|
|
18
|
+
const RAW_REQUEST_DESTRUCTURED = 'VariableDeclarator[init.name="req"] > ObjectPattern > Property[key.name=/^(body|query|params)$/]'
|
|
19
|
+
const RAW_REQUEST_ALIASED = 'VariableDeclarator[id.type="Identifier"][init.name="req"]'
|
|
20
|
+
const RAW_REQUEST_DATA = `:matches(${RAW_REQUEST_MEMBER}, ${RAW_REQUEST_DESTRUCTURED}, ${RAW_REQUEST_ALIASED})`
|
|
21
|
+
|
|
22
|
+
const RAW_SQL = 'MemberExpression[object.name="sql"]:matches([property.name="raw"], [property.value="raw"])'
|
|
23
|
+
|
|
24
|
+
const SHARED_SPECIFIER = '/^{{scope}}\\/shared(\\/|$)/'
|
|
25
|
+
const SHARED_IMPORT = `:matches(ImportDeclaration, ExportNamedDeclaration, ExportAllDeclaration, ImportExpression)[source.value=${SHARED_SPECIFIER}]`
|
|
26
|
+
const SHARED_REQUIRE = `CallExpression[callee.name="require"][arguments.0.value=${SHARED_SPECIFIER}]`
|
|
27
|
+
const SHARED_MODULE = `:matches(${SHARED_IMPORT}, ${SHARED_REQUIRE})`
|
|
12
28
|
|
|
13
29
|
function dependencyBoundary([directory, allowed]) {
|
|
14
30
|
const forbidden = WORKSPACE_PACKAGES.filter(name => !allowed.includes(name))
|
|
@@ -32,7 +48,7 @@ const dependencyBoundaries = Object.entries(ALLOWED_WORKSPACE_IMPORTS)
|
|
|
32
48
|
.map(dependencyBoundary)
|
|
33
49
|
|
|
34
50
|
const NO_PROCESS_OUTSIDE_CONFIG = {
|
|
35
|
-
selector:
|
|
51
|
+
selector: PROCESS,
|
|
36
52
|
message: 'An app touches process only in config.ts: read configuration through readConfig() and pass the value on',
|
|
37
53
|
}
|
|
38
54
|
const NO_PROCESS_ENV_IN_PACKAGE = {
|
|
@@ -48,7 +64,7 @@ const NO_RAW_SQL = {
|
|
|
48
64
|
message: 'SQL is never built from raw strings: interpolate tables and columns into the sql template instead of sql.raw',
|
|
49
65
|
}
|
|
50
66
|
const NO_SHARED_OUTSIDE_CONTRACTS = {
|
|
51
|
-
selector:
|
|
67
|
+
selector: SHARED_MODULE,
|
|
52
68
|
message: 'Contract types enter an app only through src/contracts/types.ts: import them from there',
|
|
53
69
|
}
|
|
54
70
|
|