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/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "mikoshi-construct",
3
3
  "type": "module",
4
- "version": "0.1.3",
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.com/E1i/mikoshi-construct#readme",
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
- "typecheck": "tsc --noEmit",
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
- "quality": "pnpm composition:check && pnpm lint && pnpm typecheck && pnpm test",
64
- "composition:render": "tsx scripts/composition/sync-docs.ts",
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
- Your final message is data, not prose. Return exactly one JSON object:
27
+ Return these fields; the runtime validates the shape against the schema it gives you.
28
28
 
29
- ```json
30
- {
31
- "decision": "the design in a few sentences, including what stays unchanged and why",
32
- "contractChanges": "operations or schemas to add or change in the API contract, or empty",
33
- "compositionChanges": "composition model nodes or edges to change, or empty",
34
- "constraints": ["one constraint per entry"],
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
- Your final message is data, not prose. Return exactly one JSON object:
29
+ Return these fields; the runtime validates the shape against the schema it gives you.
30
30
 
31
- ```json
32
- {
33
- "passed": true,
34
- "failureExcerpt": "the failing command and its last relevant lines, empty when passed",
35
- "securityFinding": "the invariant that failed, empty when none",
36
- "diffStat": "output of git diff --stat",
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
- Your final message is data, not prose. Return exactly one JSON object:
30
+ Return these fields; the runtime validates the shape against the schema it gives you.
31
31
 
32
- ```json
33
- {
34
- "status": "done | failed | blocked",
35
- "summary": "one or two sentences on what changed",
36
- "files": ["paths you changed"],
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
- The user's `/implement` invocation is the opt-in the tool requires.
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 `at` (ISO time), `task` (first 120 characters), `effort` (the class you chose), `status`,
30
- the rung that finished (`effort` from the result), `attempts` from the result, and the Workflow
31
- tool's own accounting for the run `agents`, `tokens`, `toolUses`, `seconds` exactly as it
32
- reported them. This log is what tunes the ladder later; workflow scripts have no filesystem
33
- access, so it is written here, not by the script. `.construct/` is gitignored.
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, only for high effort or after a blocked or failed attempt' },
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
- if (args.effort === 'high') {
139
+ async function design(rung, reason, label) {
96
140
  phase('Design')
97
- spec = await agent(architectPrompt('The task is classified as high effort; design it before any implementation.'), {
141
+ const result = await ask(architectPrompt(reason), {
98
142
  agentType: 'architect',
99
- effort: 'xhigh',
143
+ effort: DESIGN_EFFORT,
100
144
  phase: 'Design',
101
- label: 'design',
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 agent(implementerPrompt(spec, feedback), {
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: 'no report' })
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
- spec = await agent(architectPrompt(`The implementer stopped on this question:\n${report.question}`), {
127
- agentType: 'architect',
128
- effort: 'xhigh',
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 agent(harnessPrompt(), {
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({ rung, effort, outcome: passed ? 'passed' : 'failed', securityFinding: verdict?.securityFinding ?? '' })
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
- ? 'The harness produced no verdict.'
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
- spec = await agent(architectPrompt(`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.`), {
176
- agentType: 'architect',
177
- effort: 'xhigh',
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 models
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 PROCESS_ENV = 'MemberExpression[object.name="process"][property.name="env"]'
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 RAW_REQUEST_DATA = 'MemberExpression[object.name="req"][property.name=/^(body|query|params)$/]'
10
- const RAW_SQL = 'CallExpression[callee.object.name="sql"][callee.property.name="raw"]'
11
- const SHARED_IMPORT = ':matches(ImportDeclaration, ExportNamedDeclaration, ExportAllDeclaration)[source.value=/^{{scope}}\\/shared(\\/|$)/]'
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: PROCESS_MEMBER,
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: SHARED_IMPORT,
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