arkgate 3.6.1 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +122 -1145
  2. package/README.md +59 -19
  3. package/bin/ark-check-runtime.mjs +1598 -0
  4. package/bin/ark-check.mjs +32 -1565
  5. package/bin/ark-layer-match.mjs +2 -1
  6. package/bin/ark-mcp-runtime.mjs +1976 -0
  7. package/bin/ark-mcp.mjs +84 -1495
  8. package/bin/ark-shared.mjs +34 -38
  9. package/bin/ark.mjs +33 -66
  10. package/bin/lib/adapter-contract.mjs +161 -9
  11. package/bin/lib/agent-gates.mjs +1 -0
  12. package/bin/lib/analysis-completeness.mjs +28 -0
  13. package/bin/lib/analysis-engine.mjs +8 -8
  14. package/bin/lib/analysis-policy.mjs +27 -0
  15. package/bin/lib/architecture-scan.mjs +70 -304
  16. package/bin/lib/auto-patch.mjs +76 -8
  17. package/bin/lib/ci-and-commands.mjs +1 -1
  18. package/bin/lib/codex-home.mjs +43 -16
  19. package/bin/lib/design-delta.mjs +4 -0
  20. package/bin/lib/design-smells.mjs +67 -14
  21. package/bin/lib/doctor-advisories.mjs +23 -7
  22. package/bin/lib/doctor-plan.mjs +44 -47
  23. package/bin/lib/enforcement-state.mjs +2 -0
  24. package/bin/lib/github-enforcement.mjs +443 -0
  25. package/bin/lib/hook-templates.mjs +12 -148
  26. package/bin/lib/html-report-advisories.mjs +59 -0
  27. package/bin/lib/html-report-depth.mjs +9 -0
  28. package/bin/lib/html-report.mjs +5 -5
  29. package/bin/lib/install-migrate.mjs +83 -79
  30. package/bin/lib/managed-upgrade.mjs +622 -0
  31. package/bin/lib/mcp-adoption.mjs +3 -1
  32. package/bin/lib/parse-health.mjs +75 -0
  33. package/bin/lib/port-proof.mjs +2 -2
  34. package/bin/lib/prepare-change.mjs +68 -38
  35. package/bin/lib/prepare-write.mjs +7 -1
  36. package/bin/lib/reshape-decisions.mjs +284 -0
  37. package/bin/lib/resident-doctor-client.mjs +55 -0
  38. package/bin/lib/resident-hook.mjs +247 -0
  39. package/bin/lib/resolved-candidate-facts.mjs +1160 -0
  40. package/bin/lib/scan-files.mjs +19 -6
  41. package/bin/lib/snippet-analysis.mjs +119 -0
  42. package/bin/lib/source-policy.mjs +24 -0
  43. package/bin/lib/typescript-host.mjs +15 -18
  44. package/bin/lib/unavailable-analysis.mjs +76 -0
  45. package/bin/lib/upgrade-command.mjs +115 -0
  46. package/bin/lib/weakest-link.mjs +21 -179
  47. package/bin/lib/write-path-capabilities.mjs +167 -16
  48. package/bin/lib/write-path-detect.mjs +3 -2
  49. package/dist/eslint/index.cjs +3 -3
  50. package/dist/eslint/index.d.ts +3 -0
  51. package/dist/eslint/index.js +3 -3
  52. package/dist/index.cjs +7 -7
  53. package/dist/index.d.ts +1073 -141
  54. package/dist/index.js +7 -7
  55. package/docs/agent-guide.md +127 -52
  56. package/docs/ai-gates.md +100 -18
  57. package/docs/configuration.md +6 -0
  58. package/docs/demos/01-write-gate-self-correction.md +2 -2
  59. package/docs/enthusiast/README.md +10 -10
  60. package/docs/enthusiast/how-to-gallery-starter.md +2 -2
  61. package/docs/enthusiast/reference-commands.md +18 -1
  62. package/docs/enthusiast/tutorial-first-project.md +2 -2
  63. package/docs/package-surface.md +101 -14
  64. package/docs/typescript-support.md +118 -37
  65. package/package.json +33 -4
  66. package/schemas/ark.analysis-result.schema.json +159 -2
  67. package/schemas/ark.design-delta.schema.json +1 -0
  68. package/schemas/ark.enforcement-state.schema.json +84 -0
  69. package/schemas/ark.resolved-candidate-facts.schema.json +1 -0
  70. package/server.json +2 -2
  71. package/templates/skills/ark-autopilot.md +12 -0
  72. package/templates/skills/ark-explore.md +12 -5
  73. package/templates/skills/ark-fix.md +12 -2
  74. package/templates/skills/ark-loop.md +14 -1
  75. package/templates/skills/ark-runtime.md +15 -8
  76. package/templates/skills/ark-upgrade.md +122 -182
  77. package/bin/lib/ai-velocity.mjs +0 -293
  78. package/bin/lib/graph-cycles.mjs +0 -6
  79. package/bin/lib/safety-diagnostics.mjs +0 -284
  80. package/bin/lib/ts-resolve.mjs +0 -227
  81. package/dist/configTypes-DAPvBqK6.d.cts +0 -61
  82. package/dist/eslint/index.d.cts +0 -146
  83. package/dist/index.d.cts +0 -986
@@ -29,7 +29,7 @@ export const DESIGN_SMELL_OUTCOMES = Object.freeze({
29
29
  'io-under-application':
30
30
  'Business/application code reaches the database or external APIs directly — the AI will keep pasting I/O into the wrong place. Put data access behind a port/adapter.',
31
31
  'handler-in-persistence':
32
- 'HTTP handlers live under data/repository folders — names look like “storage” but they are routes. Move handlers to the API/UI layer so the AI stops mixing transport and storage.',
32
+ 'Static framework HTTP imports or route definitions live under data/repository folders — names look like “storage” but the code still owns transport. Move HTTP handling to the API/UI layer so the AI stops mixing transport and storage.',
33
33
  'god-module':
34
34
  'A few huge files own too many responsibilities — the AI cannot safely edit one concern without breaking others. Split the pilot file by job (one export surface per concern).',
35
35
  'domain-logic-in-ui':
@@ -70,6 +70,12 @@ const IO_IMPORT_RE =
70
70
  /\bfrom\s+['"](?:@?prisma\/client|@supabase\/|drizzle-orm|typeorm|knex|mongodb|pg|mysql2|better-sqlite3|ioredis|redis)['"]|require\(\s*['"](?:@?prisma\/client|pg|knex|typeorm)/;
71
71
  const HANDLER_CONTENT_RE =
72
72
  /\b(?:@Controller|@Get|@Post|@Put|@Delete|Router\(\)|createRouter|express\.Router|fastify\.(?:get|post)|export\s+(?:async\s+)?function\s+(?:GET|POST|PUT|DELETE|PATCH)\b|export\s+const\s+(?:GET|POST|PUT|DELETE|PATCH)\s*=)/;
73
+ const FRAMEWORK_HTTP_IMPORT_RE =
74
+ /(?:^|[;\n])\s*(?:import\s+(?:type\s+)?(?:[^;]{0,512}?\s+from\s+)?|export\s+(?:type\s+)?[^;]{0,512}?\s+from\s+)['"]next\/server(?:\.js)?['"]/;
75
+ const ROUTE_DEFINITION_CALL_RE =
76
+ /\bdefineRoute\s*(?:<[\s\S]{1,512}?>)?\s*\(/;
77
+ const ROUTE_DEFINITION_DECLARATION_RE =
78
+ /\b(?:export\s+)?(?:declare\s+)?(?:async\s+)?function\s+defineRoute\s*(?:<[\s\S]{1,512}?>)?\s*\(/g;
73
79
  const DOMAIN_LOGIC_UI_RE =
74
80
  /\b(?:export\s+)?(?:async\s+)?function\s+(?:can|calculate|compute|should)[A-Z]\w*|\b(?:export\s+)?const\s+(?:can|calculate|compute|should)[A-Z]\w*\s*=/;
75
81
  const EXPORT_RE =
@@ -139,7 +145,33 @@ function isPresentationLayer(name) {
139
145
  function isPersistenceLayer(name) {
140
146
  return (
141
147
  typeof name === 'string' &&
142
- (/persist|repository|infra|data.?access/i.test(name) || name === 'PersistenceAdapters')
148
+ /persist|repository|data.?access/i.test(name)
149
+ );
150
+ }
151
+
152
+ function stripObviousCommentsAndTemplates(source) {
153
+ return source
154
+ .replace(/\/\*[\s\S]*?\*\//g, ' ')
155
+ .replace(/\/\/[^\n]*/g, ' ')
156
+ .replace(/`(?:\\[\s\S]|[^\\`])*`/g, ' ');
157
+ }
158
+
159
+ function hasFrameworkHttpImport(source) {
160
+ return FRAMEWORK_HTTP_IMPORT_RE.test(stripObviousCommentsAndTemplates(source));
161
+ }
162
+
163
+ function hasRouteDefinitionCall(source) {
164
+ const code = stripObviousCommentsAndTemplates(source)
165
+ .replace(/(["'])(?:\\.|(?!\1)[^\\\r\n])*\1/g, ' ')
166
+ .replace(ROUTE_DEFINITION_DECLARATION_RE, 'function __ark_defineRoute_declaration__(');
167
+ return ROUTE_DEFINITION_CALL_RE.test(code);
168
+ }
169
+
170
+ function hasHollowPersistenceShape(source) {
171
+ return (
172
+ hasFrameworkHttpImport(source) ||
173
+ hasRouteDefinitionCall(source) ||
174
+ HANDLER_CONTENT_RE.test(source)
143
175
  );
144
176
  }
145
177
 
@@ -172,6 +204,24 @@ export function detectDesignSmells(root, config, files = [], coverage = null) {
172
204
  if (rel.includes('node_modules/') || rel.endsWith('.d.ts')) continue;
173
205
  relFiles.push(rel);
174
206
  }
207
+ // Y02: the general scan cap must not hide Persistence candidates in large
208
+ // trees. Filter by the stable role/path heuristics first, then bound reads.
209
+ const persistenceUniverse = [];
210
+ for (const f of files) {
211
+ const rel = normalizeRel(resolvedRoot, f);
212
+ if (!rel || rel.startsWith('..')) continue;
213
+ if (!/\.(ts|tsx|js|jsx|mts|cts)$/.test(rel)) continue;
214
+ if (rel.includes('node_modules/') || rel.endsWith('.d.ts')) continue;
215
+ persistenceUniverse.push(rel);
216
+ }
217
+ const allPersistenceCandidates = [...new Set(persistenceUniverse)].sort().filter(
218
+ (rel) =>
219
+ PERSISTENCE_PATH_RE.test(rel) ||
220
+ isPersistenceLayer(layerNameFor(resolvedRoot, rel, config))
221
+ );
222
+ const persistenceCandidates = allPersistenceCandidates.slice(0, MAX_SCAN_FILES);
223
+ const persistenceCandidatesTruncated =
224
+ allPersistenceCandidates.length - persistenceCandidates.length;
175
225
 
176
226
  // soft-contract: layers with files but no rule edges
177
227
  const withoutRules = Array.isArray(coverage?.layersWithoutRules)
@@ -220,8 +270,7 @@ export function detectDesignSmells(root, config, files = [], coverage = null) {
220
270
  }
221
271
  if (/\/(?:domain|application|infrastructure|adapters)\//.test(rel)) hasHexPorts = true;
222
272
 
223
- const abs = path.join(resolvedRoot, rel);
224
- const source = readTextLimited(abs);
273
+ const source = readTextLimited(path.join(resolvedRoot, rel));
225
274
  if (source == null) continue;
226
275
 
227
276
  const layer = layerNameFor(resolvedRoot, rel, config);
@@ -232,13 +281,6 @@ export function detectDesignSmells(root, config, files = [], coverage = null) {
232
281
  godEvidence.push(rel);
233
282
  }
234
283
 
235
- if (
236
- (PERSISTENCE_PATH_RE.test(rel) || isPersistenceLayer(layer)) &&
237
- HANDLER_CONTENT_RE.test(source)
238
- ) {
239
- handlerInPersist.push(rel);
240
- }
241
-
242
284
  if ((UI_PATH_RE.test(rel) || isPresentationLayer(layer)) && DOMAIN_LOGIC_UI_RE.test(source)) {
243
285
  domainInUi.push(rel);
244
286
  }
@@ -257,6 +299,13 @@ export function detectDesignSmells(root, config, files = [], coverage = null) {
257
299
  }
258
300
  }
259
301
 
302
+ for (const rel of persistenceCandidates) {
303
+ const source = readTextLimited(path.join(resolvedRoot, rel));
304
+ if (source != null && hasHollowPersistenceShape(source)) {
305
+ handlerInPersist.push(rel);
306
+ }
307
+ }
308
+
260
309
  if (!falseGreen?.risk && ioUnderAppFiles.length > 0) {
261
310
  smells.push(
262
311
  makeDesignSmell({
@@ -270,13 +319,17 @@ export function detectDesignSmells(root, config, files = [], coverage = null) {
270
319
  }
271
320
 
272
321
  if (handlerInPersist.length > 0) {
322
+ const capNote =
323
+ persistenceCandidatesTruncated > 0
324
+ ? `; ${persistenceCandidatesTruncated} more Persistence candidate(s) were not inspected by the bounded scan`
325
+ : '';
273
326
  smells.push(
274
327
  makeDesignSmell({
275
328
  id: 'handler-in-persistence',
276
329
  severity: 'warn',
277
- message: `HTTP/route handler shape found under persistence/repository paths (${handlerInPersist.length} file(s)) — semantic false-green risk.`,
330
+ message: `Static framework HTTP import or route-definition/handler shape found in Persistence-role modules (${handlerInPersist.length} file(s)${capNote}) — semantic false-green risk.`,
278
331
  evidence: handlerInPersist.slice(0, 12),
279
- fix: 'Move handlers to Presentation/API; keep Persistence as data access only (/ark-explore shape-focus).',
332
+ fix: 'Move HTTP imports and route definitions to Presentation/API; keep Persistence as data access only (/ark-explore shape-focus).',
280
333
  })
281
334
  );
282
335
  }
@@ -414,7 +467,7 @@ function successSignalFor(id) {
414
467
  case 'io-under-application':
415
468
  return '0 Application-layer files import prisma/supabase/drizzle/pg clients; I/O behind ports';
416
469
  case 'handler-in-persistence':
417
- return '0 HTTP handler shapes under persistence/repository globs';
470
+ return '0 static framework HTTP imports or route-definition/handler shapes in Persistence-role modules';
418
471
  case 'god-module':
419
472
  return 'Pilot god module split; fan-in and export surface reduced without new edge violations';
420
473
  case 'domain-logic-in-ui':
@@ -1,24 +1,38 @@
1
1
  /**
2
- * Doctor's advisory sensors, aggregated (W01 contract health + U05 ambient
3
- * state + X04 physical cohesion). Advisory only: nothing here feeds a
4
- * verdict, designFitness, or an exit code. One seam keeps doctor-plan.mjs
5
- * inside its module budget as new advisory surfaces land.
2
+ * Doctor's advisory sensors, aggregated (W01 contract health, U05 ambient
3
+ * state, X04 physical cohesion, Y03 parse health). These sensors do not create
4
+ * architecture violations or designFitness findings; Z02 separately maps
5
+ * parse-health evidence to analysis completeness and fail-closed exits. One
6
+ * seam keeps doctor-plan.mjs inside its module budget as new surfaces land.
6
7
  */
7
8
  import { computeAmbientState, printAmbientStateSection } from './ambient-state.mjs';
8
9
  import { computeContractHealth, printContractHealthSection } from './contract-smells.mjs';
9
10
  import {
10
11
  computePhysicalCohesion,
11
- computeReshapePilot,
12
12
  printPhysicalCohesionSection,
13
13
  } from './physical-cohesion.mjs';
14
+ import {
15
+ computeDecisionAwareReshapePilot,
16
+ computeReshapeDecisionMemory,
17
+ printReshapeDecisionsSection,
18
+ } from './reshape-decisions.mjs';
19
+ import { printParseHealthSection, summarizeParseHealth } from './parse-health.mjs';
14
20
 
15
- export function computeDoctorAdvisories(root, config, cov, rules, files, ts) {
21
+ export function computeDoctorAdvisories(root, config, cov, rules, files, ts, parseHealth) {
16
22
  const physicalCohesion = computePhysicalCohesion(root, files);
17
- physicalCohesion.reshapePilot = computeReshapePilot(physicalCohesion, files, root);
23
+ const decisionMemory = computeReshapeDecisionMemory(root, files);
24
+ physicalCohesion.reshapeDecisions = decisionMemory.summary;
25
+ physicalCohesion.reshapePilot = computeDecisionAwareReshapePilot(
26
+ physicalCohesion,
27
+ files,
28
+ root,
29
+ decisionMemory
30
+ );
18
31
  return {
19
32
  contractHealth: computeContractHealth(root, config, cov, rules),
20
33
  ambientState: computeAmbientState(ts, root, config, files),
21
34
  physicalCohesion,
35
+ parseHealth: parseHealth ?? summarizeParseHealth(),
22
36
  };
23
37
  }
24
38
 
@@ -30,4 +44,6 @@ export function printDoctorAdvisories(advisories, io) {
30
44
  advisories.physicalCohesion?.reshapePilot,
31
45
  io
32
46
  );
47
+ printReshapeDecisionsSection(advisories.physicalCohesion?.reshapeDecisions, io);
48
+ printParseHealthSection(advisories.parseHealth, io);
33
49
  }
@@ -1,6 +1,4 @@
1
- /**
2
- * Coverage, plan, and doctor CLI surfaces (roadmap #11).
3
- */
1
+ /** Coverage, plan, and doctor CLI surfaces (roadmap #11). */
4
2
  import fs from 'node:fs';
5
3
  import path from 'node:path';
6
4
  import {
@@ -41,6 +39,9 @@ import {
41
39
  import { loadGoldenPattern, summarizeGoldenPattern } from './golden-pattern.mjs';
42
40
  import { summarizePilotLoop } from './pilot-loop.mjs';
43
41
  import { computeDoctorAdvisories, printDoctorAdvisories } from './doctor-advisories.mjs';
42
+ import { ANALYSIS_COMPLETENESS, analysisIncompleteStatement, normalizeAnalysisCompleteness } from './analysis-completeness.mjs';
43
+ import { designDeltaDoctorLines } from './design-delta.mjs';
44
+ import { enforcementDoctorLines } from './enforcement-state.mjs';
44
45
 
45
46
  const color = {
46
47
  green: (s) => `\x1b[32m${s}\x1b[0m`,
@@ -49,12 +50,10 @@ const color = {
49
50
  dim: (s) => `\x1b[2m${s}\x1b[0m`,
50
51
  bold: (s) => `\x1b[1m${s}\x1b[0m`,
51
52
  };
52
-
53
53
  function normalize(value) {
54
54
  return String(value).split(path.sep).join('/');
55
55
  }
56
56
 
57
-
58
57
  export function computeCoverage(root, config, files, rules) {
59
58
  const layers = config.layers ?? [];
60
59
  const counts = new Map(layers.map((layer) => [layer.name, 0]));
@@ -154,9 +153,6 @@ export function runCoverage(root, config, files, rules, asJson) {
154
153
  // --doctor: one consolidated health view — coverage, violations, gates, skills, baseline,
155
154
  // and command runners — each with the exact command to fix it. Folds the data the other
156
155
  // modes already produce so a team sees "what state is my Ark adoption in?" at a glance.
157
- // Co-pilot Phase F — turn active violations into a classified, ordered remediation PLAN with an
158
- // embedded GOAL. This is the `plan` primitive the future apply-loop (Phase H, `loop`) consumes
159
- // and the autopilot (Phase I) drives toward the `goal`. Read-only: it changes no files.
160
156
  /**
161
157
  * @param {string} root
162
158
  * @param {object[]} activeViolations
@@ -176,11 +172,10 @@ export function buildRemediationPlan(
176
172
  totalFiles = null,
177
173
  options = {}
178
174
  ) {
179
- // A plan with 0 violations but ~0% governed (or ZERO files in scope) is a FALSE green:
180
- // nothing is actually being checked. Treat as "not done — classify / fix include first."
175
+ const completeness = normalizeAnalysisCompleteness(options.completeness);
181
176
  const governedLow = governedPercent != null && governedPercent < 50;
182
177
  const emptyScope = totalFiles === 0;
183
- const notHonestlyEnforced = governedLow || emptyScope;
178
+ const notHonestlyEnforced = governedLow || emptyScope || completeness !== ANALYSIS_COMPLETENESS.complete;
184
179
  const steps = activeViolations.map((v, index) => {
185
180
  const verdict = classifyRemediation(v);
186
181
  return {
@@ -249,9 +244,11 @@ export function buildRemediationPlan(
249
244
  statement =
250
245
  'No active edge violations — contract edges are clean, but design smells remain (ENFORCE · design-weak). Shape residual is plan B only; not healthy finished.';
251
246
  }
247
+ if (completeness !== ANALYSIS_COMPLETENESS.complete) statement = analysisIncompleteStatement(completeness);
252
248
 
253
249
  return {
254
250
  version: '1',
251
+ completeness,
255
252
  goal: {
256
253
  statement,
257
254
  // Edge remediation termination (Phase H). Design-weak does NOT flip met false
@@ -378,6 +375,8 @@ export function runPlan(
378
375
  }
379
376
 
380
377
  export function runDoctor(root, config, files, rules, violations, asJson, options = {}) {
378
+ const completeness = normalizeAnalysisCompleteness(options.completeness);
379
+ const analysisComplete = completeness === ANALYSIS_COMPLETENESS.complete;
381
380
  const cov = computeCoverage(root, config, files, rules);
382
381
  const summary = summarizeViolations(violations);
383
382
  const configPath = options.configPath ?? path.join(root, 'ark.config.json');
@@ -408,11 +407,14 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
408
407
  : 0;
409
408
  const activeCount = violations.length - suppressed;
410
409
  const designSmells = detectDesignSmells(root, config, files, cov);
411
- const designFitness = summarizeDesignFitness(designSmells, {
410
+ const observedDesignFitness = summarizeDesignFitness(designSmells, {
412
411
  activeViolations: activeCount,
413
412
  governedPercent: cov.governed.percent,
414
413
  totalFiles: cov.governed.totalFiles,
415
414
  });
415
+ const designFitness = analysisComplete ? observedDesignFitness : {
416
+ ...observedDesignFitness, status: 'analysis-incomplete', designWeak: false, label: 'Design fitness not verified — analysis is incomplete; observed smells remain advisory.',
417
+ };
416
418
  // Q01 — single post-green door when design-weak (map → B; no skill shopping).
417
419
  const postGreenPath = buildPostGreenNextAction(designFitness);
418
420
  // Q03 — optional golden pattern for NEW code (advisory; never clears design-weak).
@@ -424,17 +426,18 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
424
426
  patternBets: patternBetsForLoop,
425
427
  designSmells,
426
428
  });
427
- const { contractHealth, ambientState, physicalCohesion } = computeDoctorAdvisories(root, config, cov, rules, files, options.ts); // W01+U05+X04 advisories — never a verdict
429
+ const doctorAdvisories = computeDoctorAdvisories(root, config, cov, rules, files, options.ts, options.parseHealth);
428
430
 
429
431
  if (asJson) {
430
- console.log(
432
+ (options.writeJson ?? console.log)(
431
433
  JSON.stringify(
432
434
  {
433
- ok: true,
435
+ ok: analysisComplete && (options.designDelta?.valid ?? true),
434
436
  doctor: {
437
+ completeness,
435
438
  operatingMode: resolveOperatingMode({
436
439
  governedPercent: cov.governed.percent,
437
- planMet: activeCount === 0 && cov.governed.percent >= 50,
440
+ planMet: analysisComplete && activeCount === 0 && cov.governed.percent >= 50,
438
441
  mature: cov.governed.totalFiles >= 150,
439
442
  totalFiles: cov.governed.totalFiles,
440
443
  emptyLayers: cov.emptyLayers,
@@ -449,6 +452,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
449
452
  // Path-correct ENFORCE can still be design-weak (P02).
450
453
  designFitness,
451
454
  designSmells,
455
+ ...(options.designDelta ? { designDelta: options.designDelta } : {}),
452
456
  // Q01: primary next action when Shape residual dominates (null if not design-weak).
453
457
  postGreenPath,
454
458
  ...(postGreenPath
@@ -461,11 +465,9 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
461
465
  goldenPattern,
462
466
  // Q04: one-pilot loop (extraction card → re-doctor).
463
467
  pilotLoop,
464
- // Advisories, never a verdict: W01 contract health, U05 ambient
465
- // state (opt-in), X04 physical cohesion + proposed reshape pilot.
466
- contractHealth,
467
- ambientState,
468
- physicalCohesion,
468
+ // Advisories, never a verdict: W01 contract health, U05 ambient state,
469
+ // X04 physical cohesion/reshape pilot, Y03 parse health.
470
+ ...doctorAdvisories,
469
471
  governed: cov.governed,
470
472
  emptyLayers: cov.emptyLayers,
471
473
  layersWithoutRules: cov.layersWithoutRules,
@@ -483,13 +485,12 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
483
485
  baseline: {
484
486
  exists: baseline.exists,
485
487
  frozen: baseline.exists ? baseline.keys.size : 0,
486
- stale: staleBaseline,
488
+ stale: analysisComplete ? staleBaseline : null,
487
489
  policy: adoption.baseline,
488
490
  },
489
491
  gatesMissing,
490
492
  skillGaps,
491
493
  staleRunnerFiles: staleRunners,
492
- // Active-host guarantees plus separate repo-wide inventory.
493
494
  writePath: {
494
495
  activeHost: writePath.activeHost,
495
496
  support: writePath.support,
@@ -498,6 +499,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
498
499
  capabilityEvidence: writePath.capabilityEvidence,
499
500
  inventory: writePath.inventory,
500
501
  enforcementLadder: writePath.enforcementLadder,
502
+ enforcementState: writePath.enforcementState,
501
503
  mode: writePath.mode,
502
504
  prepareWrite: writePath.prepareWrite,
503
505
  autoPatch: writePath.autoPatch,
@@ -546,16 +548,16 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
546
548
  const bad = color.red('✗');
547
549
  const actions = [];
548
550
  const line = (mark, text) => console.log(` ${mark} ${text}`);
549
-
551
+ if (!analysisComplete) actions.push('restore complete analysis, then rerun ark-check --doctor');
550
552
  console.log(color.bold(`Ark doctor — ${path.basename(path.resolve(root)) || '.'}`));
553
+ if (!analysisComplete) line(warn, analysisIncompleteStatement(completeness));
551
554
 
552
555
  const emptyScope = cov.governed.totalFiles === 0;
553
556
  const totalFiles = cov.governed.totalFiles || 0;
554
557
  const presentationRow = cov.layers.find((r) => r.name === 'PresentationAdapters');
555
558
  const mode = resolveOperatingMode({
556
559
  governedPercent: emptyScope ? 0 : cov.governed.percent,
557
- planMet:
558
- activeCount === 0 && !emptyScope && cov.governed.percent >= 50,
560
+ planMet: analysisComplete && activeCount === 0 && !emptyScope && cov.governed.percent >= 50,
559
561
  mature: cov.governed.totalFiles >= 150,
560
562
  totalFiles: cov.governed.totalFiles,
561
563
  emptyLayers: cov.emptyLayers,
@@ -597,7 +599,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
597
599
  console.log('');
598
600
  console.log(color.bold('Design fitness'));
599
601
  if (designSmells.length === 0) {
600
- line(ok, designFitness.label);
602
+ line(analysisComplete ? ok : warn, designFitness.label);
601
603
  } else {
602
604
  line(designFitness.designWeak ? warn : warn, designFitness.label);
603
605
  for (const smell of designSmells.slice(0, 5)) {
@@ -627,6 +629,13 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
627
629
  }
628
630
  }
629
631
 
632
+ if (options.designDelta) {
633
+ console.log('');
634
+ console.log(color.bold('Design delta (opt-in)'));
635
+ for (const row of designDeltaDoctorLines(options.designDelta))
636
+ line(row.level === 'bad' ? bad : row.level === 'ok' ? ok : ' ', row.level === 'dim' ? color.dim(row.text) : row.text);
637
+ }
638
+
630
639
  // Q03 — optional golden pattern note (advisory for new code only).
631
640
  if (goldenPattern.present) {
632
641
  console.log('');
@@ -647,7 +656,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
647
656
  );
648
657
  }
649
658
 
650
- printDoctorAdvisories({ contractHealth, ambientState, physicalCohesion }, { line, warn, color }); // advisory sections
659
+ printDoctorAdvisories(doctorAdvisories, { line, warn, color }); // advisory sections
651
660
 
652
661
  console.log('');
653
662
  console.log(color.bold('Coverage'));
@@ -702,8 +711,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
702
711
  console.log('');
703
712
  console.log(color.bold('Violations'));
704
713
  if (violations.length === 0) {
705
- // Avoid false confidence when the contract barely covers the tree.
706
- if (emptyScope || cov.governed.percent < 50) {
714
+ if (!analysisComplete) line(warn, 'No reported violations contract compliance is not verified until analysis is complete');
715
+ else if (emptyScope || cov.governed.percent < 50) {
707
716
  line(
708
717
  warn,
709
718
  'No active violations — coverage is still thin, so green is not yet honest enforcement'
@@ -747,20 +756,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
747
756
  line(' ', `Active host: ${writePath.activeHost}`);
748
757
  line(' ', `Supported profile: ${writePath.supportSummary}`);
749
758
  line(wpMark, `Mode: ${writePath.mode} — ${writePathLabels[writePath.mode] || writePath.mode}`);
750
- const ladder = writePath.enforcementLadder;
751
- const state = (value) => value === true ? 'yes' : value === false ? 'no' : String(value);
752
- line(
753
- ladder.localWrite.installed ? ok : warn,
754
- `Hard hook — supported: ${state(ladder.localWrite.supported)} · installed: ${state(ladder.localWrite.installed)} · active/trusted: ${state(ladder.localWrite.active)} · bypassable: ${state(ladder.localWrite.bypassable)}`
755
- );
756
- line(
757
- warn,
758
- `Advisory MCP — supported: ${state(ladder.advisoryMcp.supported)} · installed: ${state(ladder.advisoryMcp.installed)} · active: ${state(ladder.advisoryMcp.active)} · bypassable: ${state(ladder.advisoryMcp.bypassable)}`
759
- );
760
- line(
761
- capabilities['merge-gate'] ? ok : bad,
762
- `Merge gate — supported: ${state(ladder.ciMerge.supported)} · installed: ${state(ladder.ciMerge.installed)} · active: ${state(ladder.ciMerge.active)} · bypassable: ${state(ladder.ciMerge.bypassable)} · required status: ${state(ladder.ciMerge.requiredStatus)}`
763
- );
759
+ const enforcement = writePath.enforcementState;
760
+ for (const row of enforcementDoctorLines(enforcement)) line(row.level === 'ok' ? ok : row.level === 'bad' ? bad : warn, row.text);
764
761
  line(
765
762
  capabilities['repair-payload'] ? ok : warn,
766
763
  `Repair payload at hard boundary: ${capabilities['repair-payload'] ? 'yes' : 'no'}`
@@ -814,12 +811,12 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
814
811
  console.log('');
815
812
  console.log(color.bold('Baseline'));
816
813
  if (!baseline.exists) {
817
- line(violations.length > 0 ? warn : ok, violations.length > 0 ? 'No baseline — adopting a dirty repo? freeze with --update-baseline' : 'No baseline (nothing to freeze)');
814
+ line(!analysisComplete || violations.length > 0 ? warn : ok, !analysisComplete ? 'No baseline — current violations were not fully evaluated' : violations.length > 0 ? 'No baseline — adopting a dirty repo? freeze with --update-baseline' : 'No baseline (nothing to freeze)');
818
815
  } else {
819
816
  // Baseline keys are line-agnostic, so N keys can suppress ≥N violations — label as keys
820
817
  // to avoid an apparent mismatch with the "frozen" violation count above.
821
- line(ok, `${baseline.keys.size} frozen key(s)`);
822
- if (staleBaseline > 0) {
818
+ line(analysisComplete ? ok : warn, `${baseline.keys.size} frozen key(s)${analysisComplete ? '' : ' — stale comparison not verified'}`);
819
+ if (analysisComplete && staleBaseline > 0) {
823
820
  line(warn, `${staleBaseline} stale entr(y/ies) no longer occur — tighten with --update-baseline`);
824
821
  actions.push('tighten the baseline (--update-baseline)');
825
822
  }
@@ -0,0 +1,2 @@
1
+ // Generated from enforcement-state.source.mjs — run npm run generate:packaged-tooling.
2
+ import b from"node:fs";import{createRequire as q}from"node:module";import h from"node:path";const n="unverified";function M(a){const e=h.join(a,"package.json");try{if(JSON.parse(b.readFileSync(e,"utf8"))?.name==="arkgate"&&b.statSync(h.join(a,"bin","ark-check.mjs"),{throwIfNoEntry:!1})?.isFile())return{installed:!0,source:"package.json + bin/ark-check.mjs (self-host)"}}catch{}try{const r=q(e).resolve("arkgate/package.json"),o=h.dirname(r),t=JSON.parse(b.readFileSync(r,"utf8")),l=h.join(o,"bin","ark-check.mjs");if(t?.name==="arkgate"&&b.statSync(l,{throwIfNoEntry:!1})?.isFile())return{installed:!0,source:"arkgate/package.json via project resolver"}}catch{}return{installed:!1,source:"arkgate/package.json unresolved from project"}}function S(a){return a.length>0?a:["filesystem scan (no matching configuration)"]}function g({supported:a,configuredPaths:e,installed:r,active:o,runtimeObserved:t,operation:l,operationCoverage:d,bypassable:u,required:c,hard:i,sources:s}){const p=e.length>0;return{supported:a,analyzed:!0,configured:p,installed:a&&r.installed,active:o,runtimeObserved:t,operation:l,operationCoverage:d,bypassable:u,required:c,hard:i,evidence:[...S(e).map(v=>({field:"configured",source:v,value:p})),{field:"installed",source:r.source,value:a&&r.installed},{field:"active",source:s.active,value:o},{field:"runtimeObserved",source:s.runtimeObserved,value:t},{field:"operationCoverage",source:s.operationCoverage,value:d},{field:"bypassable",source:s.bypassable,value:u},{field:"required",source:s.required,value:c},{field:"hard",source:s.hard,value:i}]}}function w(a,e){const r=M(a),o=!!e.support?.capabilities?.["hard-write"],t=!!e.support?.capabilities?.["advisory-write"],l=e.capabilityEvidence["hard-write"],d=e.capabilityEvidence["advisory-write"],u=e.capabilityEvidence["merge-gate"],c=e.enforcementLadder.localWrite,i=typeof c.operationCovered=="boolean",s=i?c.operationCovered:n,p=i&&s===!0,v=!!(o&&p&&c.hard===!0),m=i?p:o&&l.length>0&&r.installed?n:!1,k=t&&d.length>0&&r.installed?n:!1,f=!!(e.ci?.failClosed&&u.length>0),C=f&&r.installed?n:!1;return{schemaVersion:"1.1",activeHost:e.activeHost,localWrite:g({supported:o,configuredPaths:l,installed:r,active:m,runtimeObserved:i,operation:i?c.operation??null:null,operationCoverage:s,bypassable:v?!1:o&&!i?n:!0,required:n,hard:v,sources:{active:i?"observed PreToolUse attempt":"runtime observation unavailable",runtimeObserved:i?"fresh PreToolUse invocation":"runtime observation unavailable",operationCoverage:i?"active-host operation matcher":"operation not observed",bypassable:v?"observed hard write boundary":"host runtime bypass evidence unavailable",required:"local host policy unavailable",hard:v?"fresh covered active-host invocation":"hardness not proven for this invocation"}}),advisoryMcp:g({supported:t,configuredPaths:d,installed:r,active:k,runtimeObserved:!1,operation:null,operationCoverage:n,bypassable:!0,required:n,hard:!1,sources:{active:"MCP runtime observation unavailable",runtimeObserved:"doctor did not observe an MCP tool invocation",operationCoverage:"advisory MCP is caller-invoked",bypassable:"advisory MCP does not intercept every write",required:"local host policy unavailable",hard:"MCP presence is advisory and never proves a hard boundary"}}),ciMerge:g({supported:!0,configuredPaths:f?u:[],installed:r,active:C,runtimeObserved:!1,operation:"merge",operationCoverage:f?n:!1,bypassable:f?n:!0,required:n,hard:!1,sources:{active:"CI run and provider enforcement not observed",runtimeObserved:"provider evidence unavailable",operationCoverage:"required-status operation coverage unavailable",bypassable:"branch-protection evidence unavailable",required:"branch-protection evidence unavailable",hard:"merge hardness requires fresh provider evidence"}})}}function j(a,e,r,o){return{...a,...o,evidence:[...a.evidence.filter(t=>!e.includes(t.field)),...e.map(t=>({field:t,source:r,value:o[t]}))]}}function x(a,e){if(!e?.available)return a;const r=typeof e.arkCheckRequired=="boolean"?e.arkCheckRequired:n,o=!!(a.enforcementState.ciMerge.configured&&a.enforcementState.ciMerge.installed),t=r===!0?o:r===!1?!1:o?n:!1,l=t===!0?e.arkCheckSourceBound===!1?!0:n:r===!1?!0:o?n:!0,d=`GitHub branch protection (${e.repo??"repository"}:${e.branch??"default"})`,u=!0,c=r,i=t===!0&&l===!1&&c===!0,s=j(a.enforcementState.ciMerge,["active","runtimeObserved","operationCoverage","bypassable","required","hard"],d,{active:t,runtimeObserved:u,operationCoverage:c,bypassable:l,required:r,hard:i});return{...a,enforcementState:{...a.enforcementState,ciMerge:s},enforcementLadder:{...a.enforcementLadder,ciMerge:{...a.enforcementLadder.ciMerge,requiredStatus:r}}}}function y(a,e){const r=o=>o===!0?"yes":o===!1?"no":String(o);return`${a} \u2014 supported: ${r(e.supported)} \xB7 analyzed: ${r(e.analyzed)} \xB7 configured: ${r(e.configured)} \xB7 installed: ${r(e.installed)} \xB7 runtime observed: ${r(e.runtimeObserved)} \xB7 operation: ${e.operation??"none"} \xB7 operation covered: ${r(e.operationCoverage)} \xB7 active: ${r(e.active)} \xB7 bypassable: ${r(e.bypassable)} \xB7 required: ${r(e.required)} \xB7 hard: ${r(e.hard)}`}function L(a){const e=[{level:a.localWrite.active===!0?"ok":"warn",text:y("Local write",a.localWrite)},{level:"warn",text:y("Advisory MCP",a.advisoryMcp)},{level:a.ciMerge.required===!0?"ok":"warn",text:y("CI merge",a.ciMerge)}];return a.localWrite.active===n&&a.localWrite.hard===!1&&e.push({level:"bad",text:"RED FLAG: local hook assets exist, but this active-host operation was not observed at runtime; hard blocking is unverified."}),e}export{w as buildEnforcementState,L as enforcementDoctorLines,x as withCiProviderEvidence};