arkgate 3.0.1 → 3.0.3

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.
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Q04 — productized pilot loop: extraction card → one pilot → re-doctor.
3
+ *
4
+ * Selects a single next pilot from patternBets / design smells, emits an
5
+ * extraction-card payload, and compares residual after re-doctor on pilot paths.
6
+ * Judgment only — never mechanical-safe; never multi-pilot batch apply.
7
+ */
8
+ import { buildPatternBetsFromSmells } from './design-smells.mjs';
9
+
10
+ /** Stable product id for JSON / tests. */
11
+ export const PILOT_LOOP_ID = 'one-pilot-redoctor';
12
+
13
+ /** Smell priority when choosing the single next pilot (lower = earlier). */
14
+ const SMELL_PRIORITY = {
15
+ 'facade-sql-in-routes': 0,
16
+ 'io-under-application': 1,
17
+ 'handler-in-persistence': 2,
18
+ 'domain-logic-in-ui': 3,
19
+ 'god-module': 4,
20
+ 'soft-contract': 5,
21
+ 'mixed-pattern-cluster': 6,
22
+ };
23
+
24
+ const DEFAULT_DO_NOT = [
25
+ 'rewrite queries / touch schema / migrations',
26
+ 'weaken ark.config.json to silence the smell',
27
+ 'auto-apply as mechanical-safe or invent new mechanical-safe kinds',
28
+ 'big-bang the whole monorepo',
29
+ ];
30
+
31
+ /**
32
+ * Evidence entries that are real file paths (not layout: / layer: tokens).
33
+ * @param {string[]} evidence
34
+ * @returns {string[]}
35
+ */
36
+ export function fileEvidencePaths(evidence = []) {
37
+ return (evidence || []).filter(
38
+ (e) =>
39
+ typeof e === 'string' &&
40
+ e.length > 0 &&
41
+ !e.startsWith('layout:') &&
42
+ !e.startsWith('layer:') &&
43
+ !e.startsWith('rule:')
44
+ );
45
+ }
46
+
47
+ /**
48
+ * Score a pattern bet for "do this pilot first".
49
+ * Prefers concrete src/ files and higher-impact smell ids.
50
+ * @param {object} bet
51
+ * @param {number} index
52
+ */
53
+ function scoreBet(bet, index) {
54
+ const files = fileEvidencePaths(bet?.evidence);
55
+ const smellPri = SMELL_PRIORITY[bet?.smellId] ?? 50;
56
+ // Higher score wins; concrete files dominate; then smell priority; stable by index.
57
+ return files.length * 100 - smellPri * 10 - index;
58
+ }
59
+
60
+ /**
61
+ * Build extraction-card fields from one pattern bet (P03/P05 vocabulary).
62
+ * @param {object} bet
63
+ * @param {string[]} [preferredFiles]
64
+ */
65
+ export function extractionCardFromBet(bet, preferredFiles) {
66
+ if (!bet || typeof bet !== 'object') return null;
67
+ const files = preferredFiles?.length
68
+ ? preferredFiles
69
+ : fileEvidencePaths(bet.evidence);
70
+ const evidence = files.length ? files : (bet.evidence || []).slice(0, 8);
71
+ const pilotTarget =
72
+ files[0] ||
73
+ (typeof bet.pilot === 'string' ? bet.pilot : null) ||
74
+ 'src/**';
75
+
76
+ return {
77
+ id: PILOT_LOOP_ID,
78
+ patternBetId: bet.id || `pattern-b:${bet.smellId || 'unknown'}`,
79
+ smellId: bet.smellId || 'unknown',
80
+ pilot: typeof bet.pilot === 'string' ? bet.pilot : pilotTarget,
81
+ pilotTarget,
82
+ evidence,
83
+ move:
84
+ typeof bet.fix === 'string' && bet.fix.trim()
85
+ ? bet.fix.trim()
86
+ : 'Apply one bounded extraction for this smell on pilot paths only',
87
+ doNot: [...DEFAULT_DO_NOT],
88
+ successSignal:
89
+ typeof bet.successSignal === 'string'
90
+ ? bet.successSignal
91
+ : 'Smell evidence paths cleared on pilot without weakening the contract',
92
+ killSwitch:
93
+ typeof bet.killSwitch === 'string'
94
+ ? bet.killSwitch
95
+ : 'If pilot increases edge violations without design clarity, stop and re-map with /ark-explore',
96
+ neverMechanicalSafe: true,
97
+ class: 'judgment',
98
+ loopStep: 'one-pilot',
99
+ reDoctor: 'ark-check --doctor --json',
100
+ rePlan: 'ark-check --plan --json',
101
+ next:
102
+ '/ark-fix (one cluster) | /ark-autopilot (user ok on B) | re-doctor after pilot',
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Select **one** next pilot from pattern bets (or build bets from smells).
108
+ * @param {object[] | null | undefined} patternBets
109
+ * @param {{ designSmells?: object[] }} [options]
110
+ * @returns {null | ReturnType<typeof extractionCardFromBet>}
111
+ */
112
+ export function selectNextPilot(patternBets, options = {}) {
113
+ let bets = Array.isArray(patternBets) ? [...patternBets] : [];
114
+ if (bets.length === 0 && Array.isArray(options.designSmells) && options.designSmells.length) {
115
+ bets = buildPatternBetsFromSmells(options.designSmells);
116
+ }
117
+ if (bets.length === 0) return null;
118
+
119
+ let best = null;
120
+ let bestScore = -Infinity;
121
+ for (let i = 0; i < bets.length; i++) {
122
+ const bet = bets[i];
123
+ if (!bet || bet.neverMechanicalSafe === false) continue;
124
+ // Skip anything that claims mechanical-safe (honesty).
125
+ if (bet.class === 'mechanical-safe') continue;
126
+ const files = fileEvidencePaths(bet.evidence);
127
+ const sc = scoreBet(bet, i);
128
+ if (sc > bestScore) {
129
+ bestScore = sc;
130
+ best = { bet, files };
131
+ }
132
+ }
133
+ if (!best) return null;
134
+ return extractionCardFromBet(best.bet, best.files);
135
+ }
136
+
137
+ /**
138
+ * Human-readable extraction card block (P05 template parity).
139
+ * @param {ReturnType<typeof extractionCardFromBet>} card
140
+ * @returns {string | null}
141
+ */
142
+ export function formatExtractionCard(card) {
143
+ if (!card) return null;
144
+ const doNot = (card.doNot || DEFAULT_DO_NOT).map((d) => ` - ${d}`).join('\n');
145
+ return [
146
+ '### Extraction card',
147
+ `Pilot: ${card.pilotTarget || card.pilot}`,
148
+ `Smell: ${card.smellId}`,
149
+ `Move: ${card.move}`,
150
+ 'Do not:',
151
+ doNot,
152
+ `Success: ${card.successSignal}`,
153
+ `Kill-switch: ${card.killSwitch}`,
154
+ `Next: ${card.next}`,
155
+ '(Q04 pilot loop: one pilot at a time → re-doctor; never mechanical-safe)',
156
+ ].join('\n');
157
+ }
158
+
159
+ /**
160
+ * Doctor/plan JSON summary of the active pilot loop step.
161
+ * @param {{
162
+ * designWeak?: boolean,
163
+ * patternBets?: object[],
164
+ * designSmells?: object[],
165
+ * }} opts
166
+ */
167
+ export function summarizePilotLoop(opts = {}) {
168
+ const designWeak = opts.designWeak === true;
169
+ if (!designWeak) {
170
+ return {
171
+ active: false,
172
+ id: PILOT_LOOP_ID,
173
+ reason: 'not-design-weak',
174
+ oneAtATime: true,
175
+ neverMechanicalSafe: true,
176
+ };
177
+ }
178
+
179
+ const nextPilot = selectNextPilot(opts.patternBets, {
180
+ designSmells: opts.designSmells,
181
+ });
182
+ if (!nextPilot) {
183
+ return {
184
+ active: false,
185
+ id: PILOT_LOOP_ID,
186
+ reason: 'no-pattern-bets',
187
+ oneAtATime: true,
188
+ neverMechanicalSafe: true,
189
+ };
190
+ }
191
+
192
+ const remaining = Array.isArray(opts.patternBets) ? opts.patternBets.length : 0;
193
+
194
+ return {
195
+ active: true,
196
+ id: PILOT_LOOP_ID,
197
+ oneAtATime: true,
198
+ neverMechanicalSafe: true,
199
+ remainingBets: remaining,
200
+ nextPilot,
201
+ instruction:
202
+ 'Apply ONE pilot from nextPilot (extraction card), then re-doctor. ' +
203
+ 'Do not multi-pilot batch. patternBets never mechanical-safe. ' +
204
+ 'Success = reduced smell evidence on pilot paths; residual outside pilot may remain.',
205
+ cardText: formatExtractionCard(nextPilot),
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Compare design-smell residual on the pilot after a single change.
211
+ * Drives real before/after smell arrays (from detectDesignSmells).
212
+ *
213
+ * @param {{
214
+ * beforeSmells: object[],
215
+ * afterSmells: object[],
216
+ * nextPilot: { smellId: string, evidence?: string[], pilotTarget?: string, pilot?: string },
217
+ * }} args
218
+ */
219
+ export function comparePilotResidual({ beforeSmells, afterSmells, nextPilot }) {
220
+ const smellId = nextPilot?.smellId;
221
+ const pilotFiles = fileEvidencePaths(nextPilot?.evidence || []);
222
+ if (nextPilot?.pilotTarget && !pilotFiles.includes(nextPilot.pilotTarget)) {
223
+ if (
224
+ typeof nextPilot.pilotTarget === 'string' &&
225
+ !nextPilot.pilotTarget.startsWith('layout:') &&
226
+ !nextPilot.pilotTarget.includes('**')
227
+ ) {
228
+ pilotFiles.push(nextPilot.pilotTarget);
229
+ }
230
+ }
231
+
232
+ const beforeSmell = (beforeSmells || []).find((s) => s.id === smellId);
233
+ const afterSmell = (afterSmells || []).find((s) => s.id === smellId);
234
+
235
+ const beforeAll = fileEvidencePaths(beforeSmell?.evidence);
236
+ const afterAll = fileEvidencePaths(afterSmell?.evidence);
237
+
238
+ // Evidence on the pilot file set (exact path match).
239
+ const beforeOnPilot = pilotFiles.length
240
+ ? pilotFiles.filter((p) => beforeAll.includes(p))
241
+ : beforeAll;
242
+ const afterOnPilot = pilotFiles.length
243
+ ? pilotFiles.filter((p) => afterAll.includes(p))
244
+ : afterAll;
245
+
246
+ const pilotSmellCleared = !afterSmell;
247
+ const reduced =
248
+ afterOnPilot.length < beforeOnPilot.length ||
249
+ (pilotSmellCleared && beforeOnPilot.length > 0);
250
+
251
+ return {
252
+ smellId,
253
+ pilotFiles,
254
+ beforeEvidenceCount: beforeOnPilot.length,
255
+ afterEvidenceCount: afterOnPilot.length,
256
+ beforeEvidence: beforeOnPilot,
257
+ afterEvidence: afterOnPilot,
258
+ beforeSmellPresent: Boolean(beforeSmell),
259
+ afterSmellPresent: Boolean(afterSmell),
260
+ pilotSmellCleared,
261
+ reduced,
262
+ // Global residual may remain — honest Shape work.
263
+ beforeSmellCount: (beforeSmells || []).length,
264
+ afterSmellCount: (afterSmells || []).length,
265
+ };
266
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Q01 — Single post-green product path (“clarify for AI” / Shape).
3
+ *
4
+ * When edges are clean but design residual remains, doctor + agent routing name
5
+ * ONE door that chains map → dual-plan B. No skill shopping; no new skill basename.
6
+ * Plan B stays never mechanical-safe.
7
+ */
8
+
9
+ /** Stable product id for JSON / tests. */
10
+ export const POST_GREEN_PATH_ID = 'clarify-for-ai';
11
+
12
+ /** Primary skill entry (map + dual-plan seed). Apply is second step of the same path. */
13
+ export const POST_GREEN_PRIMARY_SKILL = '/ark-explore';
14
+
15
+ /**
16
+ * Canonical human / agent next-action string (single door).
17
+ * Chained: explore shape-focus then autopilot only to apply B with user OK.
18
+ */
19
+ export const POST_GREEN_PRIMARY_ACTION =
20
+ 'Clarify for AI (Shape): /ark-explore shape-focus → dual-plan B, then /ark-autopilot only to apply B with your OK — never empty plan A = done; patternBets never mechanical-safe';
21
+
22
+ /** Short label for tables / metrics. */
23
+ export const POST_GREEN_PRIMARY_SHORT =
24
+ '/ark-explore shape-focus → /ark-autopilot (apply B with OK) # clarify for AI';
25
+
26
+ /**
27
+ * @param {{ designWeak?: boolean } | null | undefined} designFitness
28
+ * @returns {null | {
29
+ * id: string,
30
+ * primary: true,
31
+ * skill: string,
32
+ * applySkill: string,
33
+ * flow: string,
34
+ * action: string,
35
+ * short: string,
36
+ * neverMechanicalSafe: true,
37
+ * healthyFinishedForbidden: true,
38
+ * }}
39
+ */
40
+ export function buildPostGreenNextAction(designFitness) {
41
+ if (!designFitness?.designWeak) return null;
42
+ return {
43
+ id: POST_GREEN_PATH_ID,
44
+ primary: true,
45
+ skill: POST_GREEN_PRIMARY_SKILL,
46
+ applySkill: '/ark-autopilot',
47
+ flow: 'shape-focus',
48
+ action: POST_GREEN_PRIMARY_ACTION,
49
+ short: POST_GREEN_PRIMARY_SHORT,
50
+ neverMechanicalSafe: true,
51
+ healthyFinishedForbidden: true,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Put the single post-green door first; drop competing Shape guidance strings.
57
+ * @param {string[]} actions
58
+ * @param {ReturnType<typeof buildPostGreenNextAction>} postGreen
59
+ * @returns {string[]}
60
+ */
61
+ export function mergePostGreenTopActions(actions, postGreen) {
62
+ const list = [...(actions || [])].filter(Boolean);
63
+ if (!postGreen?.action) return [...new Set(list)];
64
+
65
+ const competing =
66
+ /\/ark-explore|\/ark-autopilot|shape residual|dual-plan B|pattern bet|shape-focus|clarify for ai|design-weak/i;
67
+ const filtered = list.filter((a) => !competing.test(a));
68
+ return [postGreen.action, ...new Set(filtered)];
69
+ }
70
+
71
+ /**
72
+ * Whether doctor may print “Healthy — nothing to do”.
73
+ * @param {{ designWeak?: boolean } | null | undefined} designFitness
74
+ * @param {string[]} topActions
75
+ */
76
+ export function isDoctorHealthyNothingToDo(designFitness, topActions = []) {
77
+ if (designFitness?.designWeak) return false;
78
+ return !topActions.some(Boolean);
79
+ }
@@ -115,6 +115,8 @@ export function composePrepareWrite(opts) {
115
115
  ...(placement?.message ? { placementMessage: placement.message } : {}),
116
116
  ...(placement?.note ? { placementNote: placement.note } : {}),
117
117
  ...(placement?.description ? { description: placement.description } : {}),
118
+ // Q03: pass through golden pattern from ark_place (advisory; absent is normal).
119
+ ...(placement?.goldenPattern ? { goldenPattern: placement.goldenPattern } : {}),
118
120
  valid: gate.valid,
119
121
  violations: gate.violations,
120
122
  ...(gate.autoPatch ? { autoPatch: gate.autoPatch } : {}),
package/dist/index.cjs CHANGED
@@ -50,7 +50,7 @@ __export(gate_exports, {
50
50
  module.exports = __toCommonJS(gate_exports);
51
51
 
52
52
  // src/version.ts
53
- var version = "3.0.1";
53
+ var version = "3.0.3";
54
54
 
55
55
  // src/domain/adapterContract.ts
56
56
  var ARK_ANALYSIS_RESULT_SCHEMA_VERSION = "1.0";
package/dist/index.d.cts CHANGED
@@ -2,7 +2,7 @@ import { a as ArkConfigRule, b as ArkConfigLayer, A as ArkConfig, c as ArkConfig
2
2
  export { d as ARK_CONFIG_SCHEMA, e as ARK_CONFIG_SCHEMA_VERSION, l as loadArkConfigContract, p as parseArkConfigJson } from './configContract-BxSIwVRo.cjs';
3
3
 
4
4
  /** ArkGate library version — single source of truth. */
5
- declare const version = "3.0.1";
5
+ declare const version = "3.0.3";
6
6
 
7
7
  /** Versioned public result contract shared by every ArkGate enforcement adapter. */
8
8
  declare const ARK_ANALYSIS_RESULT_SCHEMA_VERSION: "1.0";
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { a as ArkConfigRule, b as ArkConfigLayer, A as ArkConfig, c as ArkConfig
2
2
  export { d as ARK_CONFIG_SCHEMA, e as ARK_CONFIG_SCHEMA_VERSION, l as loadArkConfigContract, p as parseArkConfigJson } from './configContract-BxSIwVRo.js';
3
3
 
4
4
  /** ArkGate library version — single source of truth. */
5
- declare const version = "3.0.1";
5
+ declare const version = "3.0.3";
6
6
 
7
7
  /** Versioned public result contract shared by every ArkGate enforcement adapter. */
8
8
  declare const ARK_ANALYSIS_RESULT_SCHEMA_VERSION: "1.0";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var version = "3.0.1";
2
+ var version = "3.0.3";
3
3
 
4
4
  // src/domain/adapterContract.ts
5
5
  var ARK_ANALYSIS_RESULT_SCHEMA_VERSION = "1.0";
@@ -79,12 +79,53 @@ as unresolved decisions, and restores the integration with `ark start --tools <h
79
79
  (no apply). `/ark-coverage` = Ark **fitness** only (governed/gates). `/ark-think` = one decision
80
80
  (2–3 options). `/ark-adopt` = brownfield Align/Stabilize + seed Shape B. `/ark-autopilot` =
81
81
  explore then apply A + propose/apply-with-ok B. `/ark-loop` = plan A only. Empty plan A is not
82
- “architecture healthy” if design-weak residual remains.
82
+ “architecture healthy” if design-weak residual remains. Full routing table: full-install
83
+ `AGENTS.md` / [README skill table](../README.md#other-skills-only-when-you-need-them).
84
+
85
+ **Design fitness (3.0.1+ / Phase Q 3.0.3):** after edges are clean, doctor can still report **ENFORCE · design-weak**.
86
+
87
+ ```bash
88
+ npx ark-check --doctor --json # designFitness, designSmells[].outcome, postGreenPath, goldenPattern, pilotLoop
89
+ npx ark-check --plan --json # plan.goal.designWeak + plan.patternBets[] + plan.pilotLoop
90
+ ```
91
+
92
+ **Post-green path (Q01):** when design-weak, doctor sets `postGreenPath` / `primaryNextAction`
93
+ (`clarify-for-ai`) — **one** Shape door: `/ark-explore` shape-focus → dual-plan B, then
94
+ `/ark-autopilot` only to apply B with your OK. Do not skill-shop coverage/think for the same residual.
95
+
96
+ **Pilot loop (Q04):** when design-weak, `pilotLoop.nextPilot` is **one** extraction card
97
+ (pilot target, move, success, kill-switch). Apply **that one pilot only**, then re-doctor.
98
+ Success = reduced smell evidence on pilot paths; residual outside the pilot may remain.
99
+ Never multi-pilot batch; never mechanical-safe; never claim healthy finished while design-weak.
100
+
101
+ **AI-velocity evidence (Q05):** deterministic fixture bench (no live LLM) compares the same
102
+ feature add on design-weak vs golden-path trees. Run `npm run eval:ai-velocity`; metric is
103
+ `placementTurns` (agent-equivalent steps to the DomainModel home). Method is stored next to
104
+ the number in `eval/ai-velocity-report.json`. See [eval/README.md](../eval/README.md).
105
+
106
+ Smell **ids** (stable JSON) plus **outcome** lines (plain language, Q02) on each
107
+ `designSmells[]` object — prefer `outcome` for humans; keep `id` for automation:
108
+
109
+ | id | Outcome (what to do / why the AI struggles) |
110
+ |----|-----------------------------------------------|
111
+ | `io-under-application` | Business code reaches DB/APIs directly — put I/O behind a port/adapter |
112
+ | `handler-in-persistence` | HTTP handlers under storage folders — move handlers to API/UI |
113
+ | `god-module` | Huge multi-job files — split the pilot by concern |
114
+ | `domain-logic-in-ui` | can*/calculate* in UI — move pure rules into Domain |
115
+ | `facade-sql-in-routes` | Routes import ORM/SQL — keep queries in repository/adapter |
116
+ | `mixed-pattern-cluster` | Several layout styles — pick one golden pattern + pilot |
117
+ | `soft-contract` | Layers without deny rules — add real walls, not soft green |
118
+
119
+ Each smell also has `evidence[]` paths and `message` (technical detail). Plan **B** bets include
120
+ `pilot`, `successSignal`, `killSwitch`, and **`neverMechanicalSafe: true`** — loop/autoPatch must
121
+ ignore them. For judgment I/O moves use **extraction cards**
122
+ ([brownfield-adoption.md](brownfield-adoption.md) §6). Multi-PR residual may optionally be
123
+ persisted as a short Shape plan under the repo; not a gate requirement.
83
124
 
84
125
  **Full-skill agent co-pilot:** after explicitly installing the `/ark-*` pack, use
85
126
  `/ark-autopilot` (explore-first, dual plan A remediation + B pattern bets). Recon without
86
127
  applying: `/ark-explore`. The default compact router uses MCP/CLI directly. Never treat empty
87
- `--plan` as “architecture healthy” without the explore pass.
128
+ `--plan` steps as “architecture healthy” when `designWeak` / non-empty `patternBets` remain.
88
129
 
89
130
  `ark init --archetype <id>` maps playbook ids to named presets (`hexagonal`, `layered`,
90
131
  `feature-sliced`, `monorepo`). With `--yes` and no archetype, Ark auto-selects from
@@ -208,16 +249,42 @@ reference, and explanation for the full path (recommend → init → gallery →
208
249
  5. Use `/ark-place` or `ark_place` for individual files after the contract exists.
209
250
  6. Verify with `ark-check --root . --config ark.config.json --strict`.
210
251
 
252
+ ### Golden pattern for new code (Q03)
253
+
254
+ When the team has picked **one** layout style for *new* files (after Shape / pilot),
255
+ you may record it as an optional side-car:
256
+
257
+ ```json
258
+ // .ark/golden-pattern.json
259
+ {
260
+ "schemaVersion": "1",
261
+ "name": "vertical-slice features",
262
+ "norm": "New features live under src/features/<slice>/; shared only in src/shared/.",
263
+ "newCodeHome": "src/features/",
264
+ "examplePath": "src/features/billing/createInvoice.ts"
265
+ }
266
+ ```
267
+
268
+ | Rule | Meaning |
269
+ |------|---------|
270
+ | **Optional** | Missing file is fine — no claim, no error. |
271
+ | **Advisory** | `ark_place` / `ark_prepare_write` and doctor attach `goldenPattern` for **new** code only. |
272
+ | **Not a gate** | Does **not** ENFORCE, does **not** clear design-weak, does not replace `ark.config.json`. |
273
+ | **Malformed** | Invalid JSON or missing `name`/`norm` → `invalid: true`; fix or delete — do not treat as guidance. |
274
+
275
+ Legacy paths stay migrate-on-touch; the golden norm limits where agents put **new** code.
276
+
211
277
  ### Write protocol (2.10+ / Track W)
212
278
 
213
279
  Prefer preparing the write before the host commits it to disk:
214
280
 
215
281
  | Surface | Role |
216
282
  |---------|------|
217
- | MCP **`ark_prepare_write`** | Place + constrain + validate + optional `autoPatch` + `judgmentBrief` + contentHash in one call |
283
+ | MCP **`ark_prepare_write`** | Place + constrain + validate + optional `autoPatch` + `judgmentBrief` + contentHash + optional `goldenPattern` in one call |
218
284
  | Write-gate **`autoPatch`** | Mechanical-safe **import type** rewrites only; post-patch revalidation green or discarded |
219
285
  | PreToolUse **`--hook-repair`** | On deny: `ARK_REPAIR_JSON` / `ARK_AUTOPATCH_JSON` on stderr (still exit 2 — never silent write) |
220
286
  | Doctor **`writePath`** | Reports `repair` \| `reject-only` \| `mcp-only` \| `none` for installed gates |
287
+ | Doctor **`goldenPattern`** | Optional Q03 advisory summary (`present` / `invalid`); never clears design-weak |
221
288
 
222
289
  Port-proof inject binding is **judgment** for auto-apply (signature/arity change), not write-path autoPatch.
223
290
  Full reference: [ai-gates.md](ai-gates.md). Loop-cost harness: `npm run eval:loop-cost`.
package/docs/ai-gates.md CHANGED
@@ -50,6 +50,13 @@ you pass `--force`, so review and commit only the templates that match your proj
50
50
  `autoPatch` flags), the supported profile for the active host, and the evidence actually found.
51
51
  Supported capability and installed guarantee are deliberately separate.
52
52
 
53
+ **Design fitness (3.0.1+):** the same doctor JSON may include `doctor.designFitness` and
54
+ `doctor.designSmells[]` (path evidence). Edge-clean `operatingMode: enforce` can still set
55
+ `designFitness.designWeak: true` (**ENFORCE · design-weak**). That is Shape residual, not a
56
+ write-path failure. Companion plan JSON: `plan.patternBets[]` with `neverMechanicalSafe: true`
57
+ — never treat as write-boundary `autoPatch` / mechanical-safe. See
58
+ [package-surface.md](package-surface.md) and [brownfield-adoption.md](brownfield-adoption.md) §6.
59
+
53
60
  If your project uses Codex or Grok, treat MCP registration as part of the default
54
61
  setup, not an optional extra. Ark works best when the agent can read `ark://manifest`
55
62
  before it writes code; that is the fast path to avoiding architecture drift during
@@ -117,7 +117,28 @@ ark-check --doctor --json # designFitness.designWeak + designSmells[].evidence
117
117
  ark-check --plan --json # patternBets[] with neverMechanicalSafe: true
118
118
  ```
119
119
 
120
- Fixture for CI honesty: `tests/fixtures/design-weak-enforce/` (empty plan A + non-empty B).
120
+ ### Pilot loop (Q04) one pilot at a time → re-doctor
121
+
122
+ When doctor/plan report **ENFORCE · design-weak**, JSON also includes **`pilotLoop`**:
123
+
124
+ | Field | Meaning |
125
+ |-------|---------|
126
+ | `pilotLoop.active` | `true` when design-weak and at least one pattern bet exists |
127
+ | `pilotLoop.nextPilot` | **One** extraction card (same fields as §6) ranked from `patternBets` |
128
+ | `pilotLoop.oneAtATime` | Always true — do not multi-pilot batch |
129
+ | `pilotLoop.neverMechanicalSafe` | Always true — judgment only; re-doctor is the success sensor |
130
+
131
+ **Loop:**
132
+
133
+ 1. Read `pilotLoop.nextPilot` (or fill the §6 card by hand).
134
+ 2. Apply **only** that pilot (bounded path(s) in `evidence` / `pilotTarget`).
135
+ 3. Re-run `ark-check --doctor --json` (and `--plan --json`).
136
+ 4. Success on this step = **reduced smell evidence on the pilot paths** (or that smell cleared). Residual outside the pilot may remain — that is honest Shape work, not failure.
137
+ 5. Pick the next `nextPilot` only after re-doctor; stop on kill-switch.
138
+
139
+ Do **not** claim “healthy finished” while `designWeak` remains. Do **not** auto-apply pattern bets as mechanical-safe.
140
+
141
+ Fixture for CI honesty: `tests/fixtures/design-weak-enforce/` (empty plan A + non-empty B + pilotLoop).
121
142
 
122
143
  ### Optional: durable Shape plan (multi-PR)
123
144
 
@@ -35,17 +35,22 @@ the layer globs so day-one **governed%** is real (not a false-green empty contra
35
35
  ### 2. See the plan yourself (optional)
36
36
 
37
37
  ```bash
38
- npx ark-check --plan # human view (includes Governed: N%)
39
- npx ark-check --plan --json # { ok, plan: { goal, counts, steps } }
38
+ npx ark-check --plan # human view (includes Governed: N%; pattern bets when design-weak)
39
+ npx ark-check --plan --json # { ok, plan: { goal, counts, steps, patternBets?, designSmells? } }
40
+ npx ark-check --doctor --json # designFitness / designSmells when residual is design-weak
40
41
  ```
41
42
 
42
- Each step is tagged `mechanical-safe` / `judgment` / `deferred` with a `confidence`,
43
+ Each **A** step is tagged `mechanical-safe` / `judgment` / `deferred` with a `confidence`,
43
44
  `rationale`, and often `remediationKind`. Auto-safe kinds: type-only type move, pure-type **file**
44
45
  relocate, `import type` of pure-type modules, and named type-export imports from mixed modules
45
46
  (`import-type-of-type-exports`). `goal.met` is true only when
46
47
  there are no active violations **and** governed coverage is meaningful — so a clean plan that
47
48
  checks almost nothing is not "done."
48
49
 
50
+ When edges are clean but design residual remains, JSON also sets `goal.designWeak` and
51
+ `patternBets[]` (each with `neverMechanicalSafe: true`). Those are **B** (Shape) bets — not
52
+ auto-applied. Extraction cards: [brownfield-adoption.md](../brownfield-adoption.md) §6.
53
+
49
54
  ### 3. Carry the plan out — the autopilot
50
55
 
51
56
  In your agent, run:
@@ -54,10 +59,12 @@ In your agent, run:
54
59
  /ark-autopilot
55
60
  ```
56
61
 
57
- It runs the whole flow (newbie tier): confirms the plan, hands off to `/ark-loop` to apply the
58
- `mechanical-safe` steps one at a time — **validating each with `ark-check` and rolling back any
59
- regression** — proposes each `judgment` step for a yes/no, loops until `goal.met`, and reports
60
- what was auto-applied vs proposed vs deferred. Nothing lands until you review the diff.
62
+ It runs the whole flow (newbie tier): **explore first** (map + dual plan), hands off to
63
+ `/ark-loop` for plan **A** `mechanical-safe` steps one at a time — **validating each with
64
+ `ark-check` and rolling back any regression** — proposes each A `judgment` and each B
65
+ pattern/Shape bet for a yes/no, and reports what was auto-applied vs proposed vs deferred.
66
+ Nothing lands until you review the diff. Empty A with open B is **not** “architecture healthy
67
+ finished.”
61
68
 
62
69
  Expert entry: skip the autopilot and use the pieces — `ark init` / `/ark-contract` to shape the
63
70
  contract, `ark-check --plan` for the work, `/ark-fix` for targeted fixes, `ark-check
@@ -15,13 +15,17 @@ This document is the consumer contract for **what is stable** vs **what is exper
15
15
  | Surface | How you use it | Stability notes |
16
16
  |---------|----------------|-----------------|
17
17
  | **CLI** | `arkgate` / `arkgate-check` (aliases `ark` / `ark-check`) | Flags and human text may improve; **JSON output shapes** for `--json` (check, doctor, plan, coverage, recommend) are stable within a major. Additive fields OK; removals/renames are major. |
18
- | **Doctor design fitness (P02+)** | `ark-check --doctor --json` → `doctor.designFitness`, `doctor.designSmells[]` | Additive. Stable smell `id`s: `io-under-application`, `handler-in-persistence`, `god-module`, `domain-logic-in-ui`, `facade-sql-in-routes`, `mixed-pattern-cluster`, `soft-contract`. Each smell has `evidence[]` paths and `fix`. Does **not** fail the gate by itself. |
18
+ | **Doctor design fitness (P02+)** | `ark-check --doctor --json` → `doctor.designFitness`, `doctor.designSmells[]` | Additive. Stable smell `id`s: `io-under-application`, `handler-in-persistence`, `god-module`, `domain-logic-in-ui`, `facade-sql-in-routes`, `mixed-pattern-cluster`, `soft-contract`. Each smell has `evidence[]`, `fix`, technical `message`, and plain-language **`outcome`** (Q02). Does **not** fail the gate by itself. |
19
+ | **Post-green path (Q01)** | `doctor.postGreenPath`, `doctor.primaryNextAction`, `doctor.healthyFinishedForbidden` | Additive when `designFitness.designWeak`. Single Shape door (`id: clarify-for-ai`): explore shape-focus → dual-plan B → autopilot only with OK. Never empty plan A = healthy finished. |
20
+ | **Golden pattern (Q03)** | Optional `.ark/golden-pattern.json`; doctor JSON `doctor.goldenPattern`; MCP `ark_place` / `ark_prepare_write` → `goldenPattern` | Additive, **advisory for NEW code only**. Required fields: `name`, `norm`; optional `newCodeHome`, `examplePath`, `schemaVersion`. **Absent is normal** (no claim). Never ENFORCE; never clears design-weak. Malformed → `invalid: true`, not silent guidance. |
19
21
  | **Plan pattern B (P03+)** | `ark-check --plan --json` → `plan.patternBets[]`, `plan.goal.designWeak` | Additive. Each bet: `id`, `smellId`, `pilot`, `evidence`, `successSignal`, `killSwitch`, **`neverMechanicalSafe: true`**, `class: "judgment"`. **Never** auto-applied by loop/autoPatch; not a `remediationKind` mechanical-safe. `goal.met` remains edge honesty only. |
22
+ | **Pilot loop (Q04)** | `plan.pilotLoop` / `doctor.pilotLoop` | Additive. When design-weak: `active`, `oneAtATime`, `neverMechanicalSafe`, **`nextPilot`** extraction-card fields (`pilotTarget`, `smellId`, `move`, `successSignal`, `killSwitch`, `doNot[]`). **One pilot → re-doctor**; never multi-pilot batch; never mechanical-safe. |
23
+ | **AI-velocity eval (Q05)** | `npm run eval:ai-velocity` → `eval/ai-velocity-report.json` | Fixture-measured (no live LLM). Same feature scenario on design-weak vs golden-path arms; metric **`placementTurns`** (agent-equivalent). Golden must be strictly better. Method string lives next to the number. Does not weaken the gate. |
20
24
  | **MCP tools** | `arkgate-mcp` / `ark://…` resources | Tool names and primary argument shapes are stable within a major. |
21
25
  | **`ark.config.json`** | Layer globs, rules, include/exclude, forbiddenGlobals, intent prefixes, `peerIsolation`, `dynamicImportAllowlist`, `safety` thresholds | Versioned by `schemaVersion`; unknown fields fail closed and migrations preserve the previous supported major. |
22
26
  | **`arkgate/schema/analysis-result`** | Public CLI/MCP/hook diagnostic envelope (`schemaVersion`, `valid`, `diagnostics`) | Versioned JSON Schema; committed v1 compatibility fixture protects rule, severity, location, and evidence fields. |
23
27
  | **Config JSON Schema** | `arkgate/schema` or `arkgate/schema/ark.config.schema.json` | Stable package resource subpaths for editor completion and contract tooling. |
24
- | **Agent skills** | `/ark-*` templates installed by `--install-agent-gates` | Skill *names* and “default flow” are stable; internal skill prose may evolve (e.g. explore dual-plan seed, day-zero origin order). |
28
+ | **Agent skills** | `/ark-*` templates installed by `--install-agent-gates` | Skill *names* and “default flow” are stable; internal skill prose may evolve (e.g. When/not when, explore Shape dual-plan seed, extraction cards, day-zero origin order). |
25
29
  | **ESLint subpath** | `arkgate/eslint` | Config-driven layer/import rules; loads consumer `ark.config.json`. |
26
30
  | **GitHub Action** | `pedroknigge/arkgate` (see `action.yml`) | The `uses:` tag/SHA selects the checker source; `version` remains an optional exact npm compatibility override. |
27
31
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkgate",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "ArkGate — architecture co-pilot for AI TypeScript (write gate, CI gate, plan/loop)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -104,6 +104,7 @@
104
104
  "eval:corpus": "node eval/validate-corpus.mjs",
105
105
  "eval:comparative": "node eval/comparative-run.mjs",
106
106
  "eval:loop-cost": "node eval/loop-cost-run.mjs",
107
+ "eval:ai-velocity": "node eval/ai-velocity-run.mjs",
107
108
  "eval:adoption": "node eval/adoption-run.mjs",
108
109
  "test:adoption-harness": "vitest run tests/unit/eval/adoptionHarness.test.ts",
109
110
  "bench:scale": "node scripts/ark-scale-bench.mjs",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/pedroknigge/arkgate",
7
7
  "source": "github"
8
8
  },
9
- "version": "3.0.1",
9
+ "version": "3.0.3",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "arkgate",
14
- "version": "3.0.1",
14
+ "version": "3.0.3",
15
15
  "runtimeHint": "npx",
16
16
  "transport": {
17
17
  "type": "stdio"
@@ -20,8 +20,12 @@ the explore pass and dual-plan section B (pattern / Shape bets).
20
20
  | “Make architecture sound” end-to-end | Map only, no apply → `/ark-explore` |
21
21
  | Brownfield or greenfield with apply | Only fitness numbers → `/ark-coverage` |
22
22
  | User wants A + B planned and A executed | Single edge fix → `/ark-fix`; plan A only → `/ark-loop` |
23
+ | **Apply half of Q01 post-green path** (after explore map / when user wants full apply) | Skipping explore when doctor primary is Shape map-first |
23
24
  | Spaghetti under ENFORCE: Shape work with user ok on B | Contract false-green first → `/ark-adopt` / `/ark-contract` STOP paths |
24
25
 
26
+ **Q01:** doctor’s single door is `/ark-explore` shape-focus → dual-plan B, **then** this skill only
27
+ to apply B with OK. Prefer that order when `postGreenPath` / design-weak is the primary residual.
28
+
25
29
  ## Related onboarding
26
30
 
27
31
  - **Greenfield:** `/ark-architect` or `ark-check --recommend` / `ark start`.
@@ -47,6 +51,9 @@ decision-grade explore pass **and** without opening violating files.
47
51
  4. **Open every file** in plan A `steps[]` (and `target` if present) before classifying a fix.
48
52
  5. **“Así te lo re-soluciono”** for each A cluster and each B pattern bet.
49
53
  6. Apply A → re-run ark-check → rollback on regression. **Never auto-apply B** as mechanical-safe.
54
+ 7. **Q04 pilot loop for B:** when design-weak, take **`pilotLoop.nextPilot`** (one extraction card)
55
+ → apply **only** that pilot with user OK → **re-doctor**. Never multi-pilot batch B; residual
56
+ outside the pilot may remain and must not be called “healthy finished.”
50
57
 
51
58
 
52
59
  ## Subagent fan-out (optional, host-dependent)