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.
- package/CHANGELOG.md +122 -1145
- package/README.md +59 -19
- package/bin/ark-check-runtime.mjs +1598 -0
- package/bin/ark-check.mjs +32 -1565
- package/bin/ark-layer-match.mjs +2 -1
- package/bin/ark-mcp-runtime.mjs +1976 -0
- package/bin/ark-mcp.mjs +84 -1495
- package/bin/ark-shared.mjs +34 -38
- package/bin/ark.mjs +33 -66
- package/bin/lib/adapter-contract.mjs +161 -9
- package/bin/lib/agent-gates.mjs +1 -0
- package/bin/lib/analysis-completeness.mjs +28 -0
- package/bin/lib/analysis-engine.mjs +8 -8
- package/bin/lib/analysis-policy.mjs +27 -0
- package/bin/lib/architecture-scan.mjs +70 -304
- package/bin/lib/auto-patch.mjs +76 -8
- package/bin/lib/ci-and-commands.mjs +1 -1
- package/bin/lib/codex-home.mjs +43 -16
- package/bin/lib/design-delta.mjs +4 -0
- package/bin/lib/design-smells.mjs +67 -14
- package/bin/lib/doctor-advisories.mjs +23 -7
- package/bin/lib/doctor-plan.mjs +44 -47
- package/bin/lib/enforcement-state.mjs +2 -0
- package/bin/lib/github-enforcement.mjs +443 -0
- package/bin/lib/hook-templates.mjs +12 -148
- package/bin/lib/html-report-advisories.mjs +59 -0
- package/bin/lib/html-report-depth.mjs +9 -0
- package/bin/lib/html-report.mjs +5 -5
- package/bin/lib/install-migrate.mjs +83 -79
- package/bin/lib/managed-upgrade.mjs +622 -0
- package/bin/lib/mcp-adoption.mjs +3 -1
- package/bin/lib/parse-health.mjs +75 -0
- package/bin/lib/port-proof.mjs +2 -2
- package/bin/lib/prepare-change.mjs +68 -38
- package/bin/lib/prepare-write.mjs +7 -1
- package/bin/lib/reshape-decisions.mjs +284 -0
- package/bin/lib/resident-doctor-client.mjs +55 -0
- package/bin/lib/resident-hook.mjs +247 -0
- package/bin/lib/resolved-candidate-facts.mjs +1160 -0
- package/bin/lib/scan-files.mjs +19 -6
- package/bin/lib/snippet-analysis.mjs +119 -0
- package/bin/lib/source-policy.mjs +24 -0
- package/bin/lib/typescript-host.mjs +15 -18
- package/bin/lib/unavailable-analysis.mjs +76 -0
- package/bin/lib/upgrade-command.mjs +115 -0
- package/bin/lib/weakest-link.mjs +21 -179
- package/bin/lib/write-path-capabilities.mjs +167 -16
- package/bin/lib/write-path-detect.mjs +3 -2
- package/dist/eslint/index.cjs +3 -3
- package/dist/eslint/index.d.ts +3 -0
- package/dist/eslint/index.js +3 -3
- package/dist/index.cjs +7 -7
- package/dist/index.d.ts +1073 -141
- package/dist/index.js +7 -7
- package/docs/agent-guide.md +127 -52
- package/docs/ai-gates.md +100 -18
- package/docs/configuration.md +6 -0
- package/docs/demos/01-write-gate-self-correction.md +2 -2
- package/docs/enthusiast/README.md +10 -10
- package/docs/enthusiast/how-to-gallery-starter.md +2 -2
- package/docs/enthusiast/reference-commands.md +18 -1
- package/docs/enthusiast/tutorial-first-project.md +2 -2
- package/docs/package-surface.md +101 -14
- package/docs/typescript-support.md +118 -37
- package/package.json +33 -4
- package/schemas/ark.analysis-result.schema.json +159 -2
- package/schemas/ark.design-delta.schema.json +1 -0
- package/schemas/ark.enforcement-state.schema.json +84 -0
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -0
- package/server.json +2 -2
- package/templates/skills/ark-autopilot.md +12 -0
- package/templates/skills/ark-explore.md +12 -5
- package/templates/skills/ark-fix.md +12 -2
- package/templates/skills/ark-loop.md +14 -1
- package/templates/skills/ark-runtime.md +15 -8
- package/templates/skills/ark-upgrade.md +122 -182
- package/bin/lib/ai-velocity.mjs +0 -293
- package/bin/lib/graph-cycles.mjs +0 -6
- package/bin/lib/safety-diagnostics.mjs +0 -284
- package/bin/lib/ts-resolve.mjs +0 -227
- package/dist/configTypes-DAPvBqK6.d.cts +0 -61
- package/dist/eslint/index.d.cts +0 -146
- 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
|
|
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
|
-
|
|
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
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
3
|
-
* state
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
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
|
}
|
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
466
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
751
|
-
const
|
|
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};
|