arkgate 3.7.0 → 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 (77) hide show
  1. package/CHANGELOG.md +76 -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-mcp-runtime.mjs +1976 -0
  6. package/bin/ark-mcp.mjs +84 -1495
  7. package/bin/ark-shared.mjs +34 -38
  8. package/bin/ark.mjs +33 -66
  9. package/bin/lib/adapter-contract.mjs +161 -9
  10. package/bin/lib/agent-gates.mjs +1 -0
  11. package/bin/lib/analysis-completeness.mjs +28 -0
  12. package/bin/lib/analysis-engine.mjs +8 -8
  13. package/bin/lib/analysis-policy.mjs +27 -0
  14. package/bin/lib/architecture-scan.mjs +70 -357
  15. package/bin/lib/auto-patch.mjs +76 -8
  16. package/bin/lib/ci-and-commands.mjs +1 -1
  17. package/bin/lib/codex-home.mjs +43 -16
  18. package/bin/lib/design-delta.mjs +4 -0
  19. package/bin/lib/doctor-advisories.mjs +4 -3
  20. package/bin/lib/doctor-plan.mjs +40 -41
  21. package/bin/lib/enforcement-state.mjs +2 -0
  22. package/bin/lib/github-enforcement.mjs +443 -0
  23. package/bin/lib/hook-templates.mjs +12 -148
  24. package/bin/lib/html-report-advisories.mjs +1 -1
  25. package/bin/lib/html-report-depth.mjs +9 -0
  26. package/bin/lib/html-report.mjs +5 -5
  27. package/bin/lib/install-migrate.mjs +83 -79
  28. package/bin/lib/managed-upgrade.mjs +622 -0
  29. package/bin/lib/mcp-adoption.mjs +3 -1
  30. package/bin/lib/parse-health.mjs +6 -5
  31. package/bin/lib/port-proof.mjs +2 -2
  32. package/bin/lib/prepare-change.mjs +68 -38
  33. package/bin/lib/prepare-write.mjs +7 -1
  34. package/bin/lib/resident-doctor-client.mjs +55 -0
  35. package/bin/lib/resident-hook.mjs +247 -0
  36. package/bin/lib/resolved-candidate-facts.mjs +1160 -0
  37. package/bin/lib/scan-files.mjs +19 -6
  38. package/bin/lib/snippet-analysis.mjs +119 -0
  39. package/bin/lib/source-policy.mjs +24 -0
  40. package/bin/lib/typescript-host.mjs +15 -18
  41. package/bin/lib/unavailable-analysis.mjs +76 -0
  42. package/bin/lib/upgrade-command.mjs +115 -0
  43. package/bin/lib/weakest-link.mjs +21 -179
  44. package/bin/lib/write-path-capabilities.mjs +167 -16
  45. package/bin/lib/write-path-detect.mjs +3 -2
  46. package/dist/eslint/index.cjs +3 -3
  47. package/dist/eslint/index.d.ts +3 -0
  48. package/dist/eslint/index.js +3 -3
  49. package/dist/index.cjs +7 -7
  50. package/dist/index.d.ts +1073 -141
  51. package/dist/index.js +7 -7
  52. package/docs/agent-guide.md +103 -59
  53. package/docs/ai-gates.md +97 -16
  54. package/docs/demos/01-write-gate-self-correction.md +2 -2
  55. package/docs/enthusiast/README.md +10 -10
  56. package/docs/enthusiast/how-to-gallery-starter.md +2 -2
  57. package/docs/enthusiast/reference-commands.md +18 -1
  58. package/docs/enthusiast/tutorial-first-project.md +2 -2
  59. package/docs/package-surface.md +98 -12
  60. package/docs/typescript-support.md +109 -37
  61. package/package.json +32 -4
  62. package/schemas/ark.analysis-result.schema.json +159 -2
  63. package/schemas/ark.design-delta.schema.json +1 -0
  64. package/schemas/ark.enforcement-state.schema.json +84 -0
  65. package/schemas/ark.resolved-candidate-facts.schema.json +1 -0
  66. package/server.json +2 -2
  67. package/templates/skills/ark-explore.md +5 -5
  68. package/templates/skills/ark-fix.md +1 -1
  69. package/templates/skills/ark-runtime.md +15 -8
  70. package/templates/skills/ark-upgrade.md +122 -182
  71. package/bin/lib/ai-velocity.mjs +0 -293
  72. package/bin/lib/graph-cycles.mjs +0 -6
  73. package/bin/lib/safety-diagnostics.mjs +0 -284
  74. package/bin/lib/ts-resolve.mjs +0 -228
  75. package/dist/configTypes-DAPvBqK6.d.cts +0 -61
  76. package/dist/eslint/index.d.cts +0 -146
  77. package/dist/index.d.cts +0 -986
@@ -0,0 +1,4 @@
1
+ // Generated from design-delta.source.mjs — run npm run generate:packaged-tooling.
2
+ import{spawnSync as R}from"node:child_process";import M from"node:crypto";import N from"node:fs";import b from"node:path";import{layerForFile as E}from"../ark-shared.mjs";import{loadGoldenPattern as C}from"./golden-pattern.mjs";import{collectGovernedFiles as O}from"./scan-files.mjs";const K="1.0",A=Object.freeze(["domain-logic-in-ui"]),x=/\.[cm]?[jt]sx?$/i,j=/(?:^|\/)(?:components?|pages|hooks|ui|views|screens)(?:\/|$)|(?:^|\/)app\/(?!api\/)/i,q=/^(can|should|calculate|compute)[A-Z_]|policy/i,W=/^(can|should)[A-Z_]|policy/i,U=/^(calculate|compute)[A-Z_]/i,I=/(?:route|routing|path|label|className|style|render|display|view|modal|dialog|tooltip|navigate|navigation|href|tab|menu|component|toast|breadcrumb|sidebar|drawer|popover|layout|theme|icon)/i,z=/^(?:use[A-Z_]|render|navigate|redirect|push|replace|open|close|show|hide|setState|set[A-Z_]|toast|alert|confirm)/,B=new Set(["includes","some","every","has"]);function m(e){return String(e||"").replace(/\\/g,"/").replace(/^\.\//,"")}function w(e){return`sha256:${M.createHash("sha256").update(e,"utf8").digest("hex")}`}function k(e,t){const i=[...t].map(n=>[m(n.path),w(n.content)]).sort(([n],[r])=>n.localeCompare(r));return w(JSON.stringify({config:e,files:i}))}function H(e,t){const i=t.toLowerCase();return i.endsWith(".tsx")?e.ScriptKind.TSX:i.endsWith(".jsx")?e.ScriptKind.JSX:i.endsWith(".js")||i.endsWith(".mjs")||i.endsWith(".cjs")?e.ScriptKind.JS:e.ScriptKind.TS}function G(e,t,i){const n=E(e,i,t?.layers??[]);return j.test(i)||/presentation|ui|view/i.test(n??"")}function J(e,t){const i=[],n=r=>{if(e.isFunctionDeclaration(r)&&r.name&&r.body)i.push({name:r.name.text,body:r.body,node:r});else if(e.isVariableDeclaration(r)&&e.isIdentifier(r.name)){const s=r.initializer;s&&(e.isArrowFunction(s)||e.isFunctionExpression(s))&&i.push({name:r.name.text,body:s.body,node:r})}else e.isMethodDeclaration(r)&&r.name&&e.isIdentifier(r.name)&&r.body&&i.push({name:r.name.text,body:r.body,node:r});e.forEachChild(r,n)};return n(t),i}function V(e,t){if(!q.test(t.name)||I.test(t.name))return null;let i=0,n=0,r=0,s=0,o=!1,a=!1;const d=new Set([e.SyntaxKind.EqualsEqualsToken,e.SyntaxKind.EqualsEqualsEqualsToken,e.SyntaxKind.ExclamationEqualsToken,e.SyntaxKind.ExclamationEqualsEqualsToken,e.SyntaxKind.LessThanToken,e.SyntaxKind.LessThanEqualsToken,e.SyntaxKind.GreaterThanToken,e.SyntaxKind.GreaterThanEqualsToken,e.SyntaxKind.InKeyword,e.SyntaxKind.InstanceOfKeyword]),u=new Set([e.SyntaxKind.AmpersandAmpersandToken,e.SyntaxKind.BarBarToken,e.SyntaxKind.QuestionQuestionToken]),f=new Set([e.SyntaxKind.PlusToken,e.SyntaxKind.MinusToken,e.SyntaxKind.AsteriskToken,e.SyntaxKind.SlashToken,e.SyntaxKind.PercentToken,e.SyntaxKind.AsteriskAsteriskToken]),c=l=>{if(e.isJsxElement(l)||e.isJsxSelfClosingElement(l)||e.isJsxFragment(l)){a=!0;return}if(e.isBinaryExpression(l)&&(d.has(l.operatorToken.kind)&&(i+=1),u.has(l.operatorToken.kind)&&(n+=1),f.has(l.operatorToken.kind)&&(r+=1)),e.isCallExpression(l)){let S="";e.isIdentifier(l.expression)?S=l.expression.text:e.isPropertyAccessExpression(l.expression)&&(S=l.expression.name.text),B.has(S)&&(s+=1),(z.test(S)||I.test(S))&&(o=!0)}e.forEachChild(l,c)};if(c(t.body),a||o)return null;const p=i+n+s,g=r;let h,y;if(U.test(t.name)&&g>0)h="calculation-rule",y=g;else if(W.test(t.name)&&p>0)h="authorization-policy-rule",y=p;else return null;return{kind:h,magnitude:y,detail:`comparisons:${i};logical:${n};predicates:${s};arithmetic:${r}`}}function Z(e,t){return e?.present&&e.golden?.newCodeHome?`Move ${t} to ${e.golden.newCodeHome} following golden pattern "${e.golden.name}", then import the pure rule from the UI.`:`Move ${t} into the project's Domain/shared pure-rules home and import it from the UI; do not weaken ark.config.json.`}function v({root:e,config:t,records:i,ts:n,goldenPattern:r}){if(!n?.createSourceFile)throw new Error("TypeScript parser is required for design-delta analysis.");const s=[];for(const o of[...i].sort((a,d)=>m(a.path).localeCompare(m(d.path)))){const a=m(o.path);if(!x.test(a)||a.endsWith(".d.ts")||!G(e,t,a))continue;const d=n.createSourceFile(a,String(o.content),n.ScriptTarget.Latest,!0,H(n,a));for(const u of J(n,d)){const f=V(n,u);if(!f)continue;const c=`domain-logic-in-ui|${a}|${u.name}|${f.kind}`,p=d.getLineAndCharacterOfPosition(u.node.getStart(d)).line+1;s.push({smellId:"domain-logic-in-ui",fingerprint:w(c),identity:c,evidence:{kind:f.kind,path:a,line:p,symbol:u.name,detail:f.detail,magnitude:f.magnitude},repairHint:Z(r,u.name)})}}return s.sort((o,a)=>o.identity.localeCompare(a.identity))}function _({mode:e,baseIdentity:t,candidateIdentity:i,touchedPaths:n,baseFindings:r,candidateFindings:s}){const o=new Set([...n].map(m)),a=new Map(r.map(c=>[c.identity,c])),d=new Set(r),u=[];let f=0;for(const c of s){let p=a.get(c.identity);if(!p){const y=[...d].filter(l=>l.smellId===c.smellId&&l.evidence.symbol===c.evidence.symbol&&l.evidence.kind===c.evidence.kind);y.length===1&&([p]=y)}p&&d.delete(p);const g=p?.evidence?.magnitude??0,h=c.evidence.magnitude;if(!o.has(c.evidence.path)){p&&(f+=1);continue}p?h>g?u.push({...c,classification:"worsened",baseMagnitude:g,candidateMagnitude:h}):f+=1:u.push({...c,classification:"new",baseMagnitude:0,candidateMagnitude:h})}return{schemaVersion:K,mode:e,complete:!0,valid:u.length===0,base:t,candidate:i,supportedSmellIds:[...A],touchedPaths:[...o].sort(),changes:u,baseFindingCount:r.length,candidateFindingCount:s.length,historicalResidualCount:f}}function D(e,t){const i=[];for(const n of O(e,t)){const r=m(b.relative(e,n));!x.test(r)||r.endsWith(".d.ts")||i.push({path:r,content:N.readFileSync(n,"utf8")})}return i}function Q(e,t,i,n){const r=new Map(i.map(s=>[m(s.path),s.content]));for(const s of n){const o=m(s.path);!x.test(o)||o.endsWith(".d.ts")||!E(e,o,t?.layers??[])||(s.delete===!0?r.delete(o):typeof s.content=="string"&&r.set(o,s.content))}return[...r].map(([s,o])=>({path:s,content:o}))}function oe({root:e,config:t,changes:i,ts:n}){const r=D(e,t),s=Q(e,t,r,i??[]),o=C(e),a=v({root:e,config:t,records:r,ts:n,goldenPattern:o}),d=v({root:e,config:t,records:s,ts:n,goldenPattern:o});return _({mode:"write-candidate",baseIdentity:{kind:"candidate-tree",value:k(t,r)},candidateIdentity:{kind:"candidate-tree",value:k(t,s)},touchedPaths:(i??[]).map(u=>u.path),baseFindings:a,candidateFindings:d})}function T(e,t,i={}){return R("git",t,{cwd:e,encoding:i.encoding??"utf8",maxBuffer:64*1024*1024,input:i.input})}function P(e,t,i){const n=T(e,t);if(n.status!==0)throw new Error(`${i}: ${(n.stderr||n.stdout||"git command failed").trim()}`);return n.stdout.trim()}function $(e,t,i){const n=T(e,t,{encoding:"buffer"});if(n.status!==0)throw new Error(`${i}: ${String(n.stderr||n.stdout||"git command failed").trim()}`);return n.stdout.toString("utf8").split("\0").map(m).filter(Boolean)}function L(e,t,i){const n=T(e,["show",`${t}:${i}`]);if(n.status!==0)throw new Error(`base file unavailable (${i}): ${(n.stderr||"").trim()}`);return n.stdout}function F(e,t){return{schemaVersion:K,mode:"git-base",complete:!1,valid:!1,base:{kind:"git-tree",value:String(e||"<missing>")},candidate:{kind:"candidate-tree",value:"<unavailable>"},supportedSmellIds:[...A],touchedPaths:[],changes:[],baseFindingCount:0,candidateFindingCount:0,historicalResidualCount:0,error:t}}function X({root:e,config:t,configPath:i="ark.config.json",baseRef:n,ts:r}){if(typeof n!="string"||!n.trim())return F(n,"--fail-on-new-smells requires --base-ref <git-ref>.");try{if(n.startsWith("-"))throw new Error('base ref must not start with "-".');const s=P(e,["rev-parse","--verify",`${n}^{commit}`],"base ref is unresolvable"),o=P(e,["rev-parse","--verify",`${s}^{tree}`],"base tree is unresolvable"),a=m(b.isAbsolute(i)?b.relative(e,i):i);if(!a||a.startsWith("../"))throw new Error("base config path must be inside the project root.");const d=JSON.parse(L(e,s,a)),f=$(e,["ls-tree","-r","--name-only","-z",s],"base tree listing failed").filter(l=>x.test(l)&&!l.endsWith(".d.ts")).filter(l=>E(e,l,d?.layers??[])).map(l=>({path:l,content:L(e,s,l)})),c=D(e,t),p=[...$(e,["diff","--name-only","-z",s,"--"],"candidate diff failed"),...$(e,["ls-files","--others","--exclude-standard","-z"],"untracked-file scan failed")],g=C(e),h=v({root:e,config:d,records:f,ts:r,goldenPattern:g}),y=v({root:e,config:t,records:c,ts:r,goldenPattern:g});return _({mode:"git-base",baseIdentity:{kind:"git-tree",value:o,commit:s},candidateIdentity:{kind:"candidate-tree",value:k(t,c)},touchedPaths:p,baseFindings:h,candidateFindings:y})}catch(s){return F(n,s instanceof Error?s.message:String(s))}}function Y(e){return e?.complete?e.valid?`Design delta passed: 0 new/worsened supported smells across ${e.touchedPaths.length} touched path(s).`:[`Design delta blocked ${e.changes.length} new/worsened supported smell(s):`,...e.changes.map(t=>`- [${t.smellId}] ${t.evidence.path}:${t.evidence.line??1} ${t.evidence.symbol??""} (${t.classification})
3
+ Next action: ${t.repairHint}`)].join(`
4
+ `):`Design delta unavailable: ${e?.error||"unknown error"}`}function le({enabled:e,...t}){const i=e?X(t):null;return{result:i,combineEdges:({activeViolationCount:n,strictConfig:r,strictWarningCount:s,policyValid:o})=>{const a=n===0&&(!r||s===0)&&o;return{edgeValid:a,observedOk:a&&(i?.valid??!0)}},exitCode:n=>i&&!i.complete?2:n,failureText:()=>i&&!i.valid?Y(i):null}}function ce(e){return e?e.complete?e.valid?[{level:"ok",text:`0 new/worsened supported smells across ${e.touchedPaths.length} touched path(s)`}]:[{level:"bad",text:`${e.changes.length} new/worsened supported smell(s) block this candidate`},...e.changes.slice(0,5).flatMap(t=>[{level:"plain",text:`[${t.smellId}] ${t.evidence.path}:${t.evidence.line??1} ${t.evidence.symbol??""}`},{level:"dim",text:`fix: ${t.repairHint}`}])]:[{level:"bad",text:`Unavailable \u2014 ${e.error||"base/candidate evidence incomplete"}`}]:[]}export{K as DESIGN_DELTA_SCHEMA_VERSION,A as DESIGN_DELTA_SUPPORTED_SMELLS,v as analyzeDesignFindings,le as createDesignDeltaCheck,ce as designDeltaDoctorLines,X as evaluateGitDesignDelta,oe as evaluateWriteDesignDelta,Y as formatDesignDeltaBlock};
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Doctor's advisory sensors, aggregated (W01 contract health, U05 ambient
3
- * state, X04 physical cohesion, Y03 parse health). 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.
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';
@@ -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 doctorAdvisories = computeDoctorAdvisories(root, config, cov, rules, files, options.ts, options.parseHealth); // advisory only — 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
@@ -481,13 +485,12 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
481
485
  baseline: {
482
486
  exists: baseline.exists,
483
487
  frozen: baseline.exists ? baseline.keys.size : 0,
484
- stale: staleBaseline,
488
+ stale: analysisComplete ? staleBaseline : null,
485
489
  policy: adoption.baseline,
486
490
  },
487
491
  gatesMissing,
488
492
  skillGaps,
489
493
  staleRunnerFiles: staleRunners,
490
- // Active-host guarantees plus separate repo-wide inventory.
491
494
  writePath: {
492
495
  activeHost: writePath.activeHost,
493
496
  support: writePath.support,
@@ -496,6 +499,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
496
499
  capabilityEvidence: writePath.capabilityEvidence,
497
500
  inventory: writePath.inventory,
498
501
  enforcementLadder: writePath.enforcementLadder,
502
+ enforcementState: writePath.enforcementState,
499
503
  mode: writePath.mode,
500
504
  prepareWrite: writePath.prepareWrite,
501
505
  autoPatch: writePath.autoPatch,
@@ -544,16 +548,16 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
544
548
  const bad = color.red('✗');
545
549
  const actions = [];
546
550
  const line = (mark, text) => console.log(` ${mark} ${text}`);
547
-
551
+ if (!analysisComplete) actions.push('restore complete analysis, then rerun ark-check --doctor');
548
552
  console.log(color.bold(`Ark doctor — ${path.basename(path.resolve(root)) || '.'}`));
553
+ if (!analysisComplete) line(warn, analysisIncompleteStatement(completeness));
549
554
 
550
555
  const emptyScope = cov.governed.totalFiles === 0;
551
556
  const totalFiles = cov.governed.totalFiles || 0;
552
557
  const presentationRow = cov.layers.find((r) => r.name === 'PresentationAdapters');
553
558
  const mode = resolveOperatingMode({
554
559
  governedPercent: emptyScope ? 0 : cov.governed.percent,
555
- planMet:
556
- activeCount === 0 && !emptyScope && cov.governed.percent >= 50,
560
+ planMet: analysisComplete && activeCount === 0 && !emptyScope && cov.governed.percent >= 50,
557
561
  mature: cov.governed.totalFiles >= 150,
558
562
  totalFiles: cov.governed.totalFiles,
559
563
  emptyLayers: cov.emptyLayers,
@@ -595,7 +599,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
595
599
  console.log('');
596
600
  console.log(color.bold('Design fitness'));
597
601
  if (designSmells.length === 0) {
598
- line(ok, designFitness.label);
602
+ line(analysisComplete ? ok : warn, designFitness.label);
599
603
  } else {
600
604
  line(designFitness.designWeak ? warn : warn, designFitness.label);
601
605
  for (const smell of designSmells.slice(0, 5)) {
@@ -625,6 +629,13 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
625
629
  }
626
630
  }
627
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
+
628
639
  // Q03 — optional golden pattern note (advisory for new code only).
629
640
  if (goldenPattern.present) {
630
641
  console.log('');
@@ -700,8 +711,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
700
711
  console.log('');
701
712
  console.log(color.bold('Violations'));
702
713
  if (violations.length === 0) {
703
- // Avoid false confidence when the contract barely covers the tree.
704
- 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) {
705
716
  line(
706
717
  warn,
707
718
  'No active violations — coverage is still thin, so green is not yet honest enforcement'
@@ -745,20 +756,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
745
756
  line(' ', `Active host: ${writePath.activeHost}`);
746
757
  line(' ', `Supported profile: ${writePath.supportSummary}`);
747
758
  line(wpMark, `Mode: ${writePath.mode} — ${writePathLabels[writePath.mode] || writePath.mode}`);
748
- const ladder = writePath.enforcementLadder;
749
- const state = (value) => value === true ? 'yes' : value === false ? 'no' : String(value);
750
- line(
751
- ladder.localWrite.installed ? ok : warn,
752
- `Hard hook — supported: ${state(ladder.localWrite.supported)} · installed: ${state(ladder.localWrite.installed)} · active/trusted: ${state(ladder.localWrite.active)} · bypassable: ${state(ladder.localWrite.bypassable)}`
753
- );
754
- line(
755
- warn,
756
- `Advisory MCP — supported: ${state(ladder.advisoryMcp.supported)} · installed: ${state(ladder.advisoryMcp.installed)} · active: ${state(ladder.advisoryMcp.active)} · bypassable: ${state(ladder.advisoryMcp.bypassable)}`
757
- );
758
- line(
759
- capabilities['merge-gate'] ? ok : bad,
760
- `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)}`
761
- );
759
+ const enforcement = writePath.enforcementState;
760
+ for (const row of enforcementDoctorLines(enforcement)) line(row.level === 'ok' ? ok : row.level === 'bad' ? bad : warn, row.text);
762
761
  line(
763
762
  capabilities['repair-payload'] ? ok : warn,
764
763
  `Repair payload at hard boundary: ${capabilities['repair-payload'] ? 'yes' : 'no'}`
@@ -812,12 +811,12 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
812
811
  console.log('');
813
812
  console.log(color.bold('Baseline'));
814
813
  if (!baseline.exists) {
815
- 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)');
816
815
  } else {
817
816
  // Baseline keys are line-agnostic, so N keys can suppress ≥N violations — label as keys
818
817
  // to avoid an apparent mismatch with the "frozen" violation count above.
819
- line(ok, `${baseline.keys.size} frozen key(s)`);
820
- if (staleBaseline > 0) {
818
+ line(analysisComplete ? ok : warn, `${baseline.keys.size} frozen key(s)${analysisComplete ? '' : ' — stale comparison not verified'}`);
819
+ if (analysisComplete && staleBaseline > 0) {
821
820
  line(warn, `${staleBaseline} stale entr(y/ies) no longer occur — tighten with --update-baseline`);
822
821
  actions.push('tighten the baseline (--update-baseline)');
823
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};