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.
- package/CHANGELOG.md +76 -1145
- package/README.md +59 -19
- package/bin/ark-check-runtime.mjs +1598 -0
- package/bin/ark-check.mjs +32 -1565
- 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 -357
- 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/doctor-advisories.mjs +4 -3
- package/bin/lib/doctor-plan.mjs +40 -41
- 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 +1 -1
- 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 +6 -5
- 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/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 +103 -59
- package/docs/ai-gates.md +97 -16
- 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 +98 -12
- package/docs/typescript-support.md +109 -37
- package/package.json +32 -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-explore.md +5 -5
- package/templates/skills/ark-fix.md +1 -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 -228
- package/dist/configTypes-DAPvBqK6.d.cts +0 -61
- package/dist/eslint/index.d.cts +0 -146
- 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).
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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';
|
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 doctorAdvisories = computeDoctorAdvisories(root, config, cov, rules, files, options.ts, options.parseHealth);
|
|
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
|
-
|
|
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
|
|
749
|
-
const
|
|
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};
|