arkgate 4.6.4 → 4.6.6
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 +77 -2105
- package/README.md +11 -9
- package/bin/ark-check-runtime.mjs +136 -16
- package/bin/ark-mcp-runtime.mjs +18 -30
- package/bin/ark.mjs +13 -3
- package/bin/lib/adapter-contract.mjs +13 -9
- package/bin/lib/adoption-stance.mjs +104 -0
- package/bin/lib/agent-projection-command.mjs +18 -0
- package/bin/lib/agent-projection.mjs +2 -2
- package/bin/lib/analysis-engine.mjs +5 -5
- package/bin/lib/ci-and-commands.mjs +3 -3
- package/bin/lib/ci-merge-boundary.mjs +91 -0
- package/bin/lib/config-contract.mjs +2 -0
- package/bin/lib/design-delta.mjs +2 -2
- package/bin/lib/design-smells.mjs +1 -1
- package/bin/lib/diagnostic-catalog.mjs +6 -5
- package/bin/lib/doctor-advisories.mjs +2 -2
- package/bin/lib/doctor-next-actions.mjs +35 -5
- package/bin/lib/doctor-plan.mjs +164 -133
- package/bin/lib/enforcement-honesty.mjs +72 -0
- package/bin/lib/first-run-help.mjs +8 -7
- package/bin/lib/graph-blind.mjs +15 -6
- package/bin/lib/html-report-advisories.mjs +10 -2
- package/bin/lib/html-report.mjs +2 -2
- package/bin/lib/install-migrate.mjs +10 -0
- package/bin/lib/invariant-coverage.mjs +6 -2
- package/bin/lib/managed-upgrade.mjs +8 -3
- package/bin/lib/mcp-adoption.mjs +19 -0
- package/bin/lib/policy-delta-io.mjs +1 -1
- package/bin/lib/post-green-path.mjs +5 -1
- package/bin/lib/presets.mjs +22 -0
- package/bin/lib/product-copy.mjs +6 -3
- package/bin/lib/remediation.mjs +74 -10
- package/bin/lib/skill-install.mjs +2 -0
- package/bin/lib/snippet-analysis.mjs +40 -8
- package/bin/lib/start-preview.mjs +12 -22
- package/bin/lib/status-command.mjs +16 -0
- package/bin/lib/status-manifest.mjs +8 -2
- package/bin/lib/team-parliament-io.mjs +62 -2
- package/bin/lib/team-parliament.mjs +25 -5
- package/bin/lib/unavailable-analysis.mjs +1 -0
- package/dist/{configTypes-B8uIcLaG.d.ts → configTypes-l6XiwiC1.d.ts} +7 -0
- package/dist/eslint/index.cjs +3 -3
- package/dist/eslint/index.d.ts +1 -1
- package/dist/eslint/index.js +3 -3
- package/dist/index.cjs +26 -26
- package/dist/index.d.ts +20 -3
- package/dist/index.js +29 -29
- package/docs/README.md +6 -9
- package/docs/agent-guide.md +10 -0
- package/docs/ai-gates.md +12 -5
- package/docs/brownfield-adoption.md +7 -1
- package/docs/configuration.md +11 -2
- package/docs/develop.md +4 -2
- package/docs/diagnostics.md +17 -7
- package/docs/package-surface.md +6 -4
- package/docs/product-voice.md +6 -4
- package/docs/threat-model.md +2 -2
- package/docs/use.md +5 -4
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +6 -0
- package/schemas/ark.design-delta.schema.json +1 -1
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +7 -0
- package/templates/agent-skills/ark-explore/SKILL.md +6 -0
- package/templates/agent-skills/ark-place/SKILL.md +11 -4
- package/templates/agent-skills/ark-upgrade/SKILL.md +9 -2
- package/templates/skills/ark-adopt.md +7 -0
- package/templates/skills/ark-explore.md +6 -0
- package/templates/skills/ark-place.md +11 -4
- package/templates/skills/ark-upgrade.md +9 -2
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-class writePath / CI honesty file (`.ark/ci-merge-boundary.json`).
|
|
3
|
+
* Agents must read this instead of grepping node_modules dist.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
export const CI_MERGE_BOUNDARY_REL = '.ark/ci-merge-boundary.json';
|
|
9
|
+
export const CI_MERGE_BOUNDARY_SCHEMA = '1.0';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {{
|
|
13
|
+
* writePath?: object,
|
|
14
|
+
* github?: { requiredStatusConfigured?: boolean, plan?: string, canRequire?: boolean, error?: string },
|
|
15
|
+
* }} input
|
|
16
|
+
*/
|
|
17
|
+
export function buildCiMergeBoundary(input = {}) {
|
|
18
|
+
const writePath = input.writePath && typeof input.writePath === 'object' ? input.writePath : {};
|
|
19
|
+
const inventory = writePath.inventory?.hosts && typeof writePath.inventory.hosts === 'object'
|
|
20
|
+
? writePath.inventory.hosts
|
|
21
|
+
: {};
|
|
22
|
+
const perHost = {};
|
|
23
|
+
for (const [host, record] of Object.entries(inventory)) {
|
|
24
|
+
const caps = record?.capabilities && typeof record.capabilities === 'object' ? record.capabilities : {};
|
|
25
|
+
const configured = Boolean(record?.configured || caps['hard-write'] || caps['advisory-write']);
|
|
26
|
+
const fired = Boolean(writePath.enforcementState?.localWrite?.runtimeObserved);
|
|
27
|
+
const hard = caps['hard-write'] === true;
|
|
28
|
+
perHost[host] = {
|
|
29
|
+
configured,
|
|
30
|
+
fired,
|
|
31
|
+
state: configured && !fired ? 'configured-not-fired' : fired ? 'observed' : 'absent',
|
|
32
|
+
writePath: hard ? 'hard' : caps['advisory-write'] ? 'soft' : 'none',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const hookConfigured = Object.values(perHost).some((h) => h.configured);
|
|
37
|
+
const hookFired = Object.values(perHost).some((h) => h.fired);
|
|
38
|
+
const github = input.github && typeof input.github === 'object' ? input.github : {};
|
|
39
|
+
const workflowPresent = Boolean(
|
|
40
|
+
github.workflowPresent ||
|
|
41
|
+
writePath.capabilities?.['merge-gate'] ||
|
|
42
|
+
writePath.inventory?.capabilities?.['merge-gate']
|
|
43
|
+
);
|
|
44
|
+
const required = github.requiredStatusConfigured === true || github.arkCheckRequired === true;
|
|
45
|
+
const canRequire = github.canRequire !== false && github.plan !== 'free';
|
|
46
|
+
let ciState = 'absent';
|
|
47
|
+
if (workflowPresent && required) ciState = 'required';
|
|
48
|
+
else if (workflowPresent && !canRequire) ciState = 'present-but-github-free-cannot-require';
|
|
49
|
+
else if (workflowPresent) ciState = 'present-but-not-required';
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
schemaVersion: CI_MERGE_BOUNDARY_SCHEMA,
|
|
53
|
+
notAScore: true,
|
|
54
|
+
path: CI_MERGE_BOUNDARY_REL,
|
|
55
|
+
hook: {
|
|
56
|
+
configured: hookConfigured,
|
|
57
|
+
fired: hookFired,
|
|
58
|
+
state: hookConfigured && !hookFired ? 'configured-not-fired' : hookFired ? 'observed' : 'absent',
|
|
59
|
+
},
|
|
60
|
+
writePath: perHost,
|
|
61
|
+
ci: {
|
|
62
|
+
workflowPresent,
|
|
63
|
+
requiredStatusConfigured: required,
|
|
64
|
+
state: ciState,
|
|
65
|
+
},
|
|
66
|
+
githubPlan: {
|
|
67
|
+
plan: github.plan ?? (canRequire ? 'unknown' : 'free'),
|
|
68
|
+
canRequire,
|
|
69
|
+
reason: canRequire ? null : 'github-free-cannot-require',
|
|
70
|
+
},
|
|
71
|
+
hookGreenIsNotTreeGreen: true,
|
|
72
|
+
scriptedEditsBypassPreToolUse: true,
|
|
73
|
+
note:
|
|
74
|
+
'Do not reverse-engineer node_modules/arkgate/dist. This file is the honesty surface for writePath and CI.',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function writeCiMergeBoundary(root, input = {}) {
|
|
79
|
+
const payload = buildCiMergeBoundary(input);
|
|
80
|
+
const dir = path.join(root, '.ark');
|
|
81
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
82
|
+
const dest = path.join(dir, 'ci-merge-boundary.json');
|
|
83
|
+
const next = `${JSON.stringify(payload, null, 2)}\n`;
|
|
84
|
+
try {
|
|
85
|
+
if (fs.existsSync(dest) && fs.readFileSync(dest, 'utf8') === next) return payload;
|
|
86
|
+
} catch {
|
|
87
|
+
/* rewrite */
|
|
88
|
+
}
|
|
89
|
+
fs.writeFileSync(dest, next);
|
|
90
|
+
return payload;
|
|
91
|
+
}
|
package/bin/lib/design-delta.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// Generated from design-delta.source.mjs — run npm run generate:packaged-tooling.
|
|
2
|
-
import{spawnSync as
|
|
2
|
+
import{spawnSync as O}from"node:child_process";import B from"node:crypto";import j from"node:fs";import w from"node:path";import{layerForFile as T}from"../ark-shared.mjs";import{loadGoldenPattern as I}from"./golden-pattern.mjs";import{collectGovernedFiles as q}from"./scan-files.mjs";const P="1.0",_=Object.freeze(["domain-logic-in-ui"]),b=/\.[cm]?[jt]sx?$/i,W=/(?:^|\/)(?:components?|pages|hooks|ui|views|screens)(?:\/|$)|(?:^|\/)app\/(?!api\/)/i,U=/^(can|should|calculate|compute)[A-Z_]|policy/i,z=/^(can|should)[A-Z_]|policy/i,H=/^(calculate|compute)[A-Z_]/i,D=/(?: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,G=/^(?:use[A-Z_]|render|navigate|redirect|push|replace|open|close|show|hide|setState|set[A-Z_]|toast|alert|confirm)/,J=new Set(["includes","some","every","has"]);function h(e){return String(e||"").replace(/\\/g,"/").replace(/^\.\//,"")}function $(e){return`sha256:${B.createHash("sha256").update(e,"utf8").digest("hex")}`}function C(e,t){const s=[...t].map(n=>[h(n.path),$(n.content)]).sort(([n],[i])=>n.localeCompare(i));return $(JSON.stringify({config:e,files:s}))}function V(e,t){const s=t.toLowerCase();return s.endsWith(".tsx")?e.ScriptKind.TSX:s.endsWith(".jsx")?e.ScriptKind.JSX:s.endsWith(".js")||s.endsWith(".mjs")||s.endsWith(".cjs")?e.ScriptKind.JS:e.ScriptKind.TS}function Z(e,t,s){const n=T(e,s,t?.layers??[]);return W.test(s)||/presentation|ui|view/i.test(n??"")}function Q(e,t){const s=[],n=i=>{if(e.isFunctionDeclaration(i)&&i.name&&i.body)s.push({name:i.name.text,body:i.body,node:i});else if(e.isVariableDeclaration(i)&&e.isIdentifier(i.name)){const a=i.initializer;a&&(e.isArrowFunction(a)||e.isFunctionExpression(a))&&s.push({name:i.name.text,body:a.body,node:i})}else e.isMethodDeclaration(i)&&i.name&&e.isIdentifier(i.name)&&i.body&&s.push({name:i.name.text,body:i.body,node:i});e.forEachChild(i,n)};return n(t),s}function X(e,t){if(!U.test(t.name)||D.test(t.name))return null;let s=0,n=0,i=0,a=0,r=!1,o=!1;const l=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]),p=new Set([e.SyntaxKind.PlusToken,e.SyntaxKind.MinusToken,e.SyntaxKind.AsteriskToken,e.SyntaxKind.SlashToken,e.SyntaxKind.PercentToken,e.SyntaxKind.AsteriskAsteriskToken]),f=c=>{if(e.isJsxElement(c)||e.isJsxSelfClosingElement(c)||e.isJsxFragment(c)){o=!0;return}if(e.isBinaryExpression(c)&&(l.has(c.operatorToken.kind)&&(s+=1),u.has(c.operatorToken.kind)&&(n+=1),p.has(c.operatorToken.kind)&&(i+=1)),e.isCallExpression(c)){let g="";e.isIdentifier(c.expression)?g=c.expression.text:e.isPropertyAccessExpression(c.expression)&&(g=c.expression.name.text),J.has(g)&&(a+=1),(G.test(g)||D.test(g))&&(r=!0)}e.forEachChild(c,f)};if(f(t.body),o||r)return null;const m=s+n+a,v=i;let S,d;if(H.test(t.name)&&v>0)S="calculation-rule",d=v;else if(z.test(t.name)&&m>0)S="authorization-policy-rule",d=m;else return null;return{kind:S,magnitude:d,detail:`comparisons:${s};logical:${n};predicates:${a};arithmetic:${i}`}}function Y(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 E({root:e,config:t,records:s,ts:n,goldenPattern:i}){if(!n?.createSourceFile)throw new Error("TypeScript parser is required for design-delta analysis.");const a=[];for(const r of[...s].sort((o,l)=>h(o.path).localeCompare(h(l.path)))){const o=h(r.path);if(!b.test(o)||o.endsWith(".d.ts")||!Z(e,t,o))continue;const l=n.createSourceFile(o,String(r.content),n.ScriptTarget.Latest,!0,V(n,o));for(const u of Q(n,l)){const p=X(n,u);if(!p)continue;const f=`domain-logic-in-ui|${o}|${u.name}|${p.kind}`,m=l.getLineAndCharacterOfPosition(u.node.getStart(l)).line+1;a.push({smellId:"domain-logic-in-ui",fingerprint:$(f),identity:f,evidence:{kind:p.kind,path:o,line:m,symbol:u.name,detail:p.detail,magnitude:p.magnitude},repairHint:Y(i,u.name)})}}return a.sort((r,o)=>r.identity.localeCompare(o.identity))}function L({mode:e,baseIdentity:t,candidateIdentity:s,touchedPaths:n,baseFindings:i,candidateFindings:a,createdPathsOnly:r=!1,baseTreePaths:o=[]}){const l=new Set([...n].map(h)),u=new Set([...o].map(h)),p=new Map(i.map(d=>[d.identity,d])),f=new Set(i),m=[];let v=0;for(const d of a){let c=p.get(d.identity);if(!c){const y=[...f].filter(k=>k.smellId===d.smellId&&k.evidence.symbol===d.evidence.symbol&&k.evidence.kind===d.evidence.kind);y.length===1&&([c]=y)}c&&f.delete(c);const g=c?.evidence?.magnitude??0,x=d.evidence.magnitude;if(!l.has(d.evidence.path)){c&&(v+=1);continue}c?x>g?m.push({...d,classification:"worsened",baseMagnitude:g,candidateMagnitude:x}):v+=1:m.push({...d,classification:"new",baseMagnitude:0,candidateMagnitude:x})}const S=r?m.filter(d=>d.classification==="new"&&!u.has(h(d.evidence.path))):m;return{schemaVersion:P,mode:e,complete:!0,valid:S.length===0,base:t,candidate:s,supportedSmellIds:[..._],touchedPaths:[...l].sort(),changes:S,baseFindingCount:i.length,candidateFindingCount:a.length,historicalResidualCount:v,enforcementScope:r?"created-paths":"touched-new-or-worsened"}}function F(e,t){const s=[];for(const n of q(e,t)){const i=h(w.relative(e,n));!b.test(i)||i.endsWith(".d.ts")||s.push({path:i,content:j.readFileSync(n,"utf8")})}return s}function ee(e,t,s,n){const i=new Map(s.map(a=>[h(a.path),a.content]));for(const a of n){const r=h(a.path);!b.test(r)||r.endsWith(".d.ts")||!T(e,r,t?.layers??[])||(a.delete===!0?i.delete(r):typeof a.content=="string"&&i.set(r,a.content))}return[...i].map(([a,r])=>({path:a,content:r}))}function de({root:e,config:t,changes:s,ts:n}){const i=F(e,t),a=ee(e,t,i,s??[]),r=I(e),o=E({root:e,config:t,records:i,ts:n,goldenPattern:r}),l=E({root:e,config:t,records:a,ts:n,goldenPattern:r});return L({mode:"write-candidate",baseIdentity:{kind:"candidate-tree",value:C(t,i)},candidateIdentity:{kind:"candidate-tree",value:C(t,a)},touchedPaths:(s??[]).map(u=>u.path),baseFindings:o,candidateFindings:l})}function K(e,t,s={}){return O("git",t,{cwd:e,encoding:s.encoding??"utf8",maxBuffer:64*1024*1024,input:s.input})}function M(e,t,s){const n=K(e,t);if(n.status!==0)throw new Error(`${s}: ${(n.stderr||n.stdout||"git command failed").trim()}`);return n.stdout.trim()}function A(e,t,s){const n=K(e,t,{encoding:"buffer"});if(n.status!==0)throw new Error(`${s}: ${String(n.stderr||n.stdout||"git command failed").trim()}`);return n.stdout.toString("utf8").split("\0").map(h).filter(Boolean)}function R(e,t,s){const n=K(e,["show",`${t}:${s}`]);if(n.status!==0)throw new Error(`base file unavailable (${s}): ${(n.stderr||"").trim()}`);return n.stdout}function N(e,t){return{schemaVersion:P,mode:"git-base",complete:!1,valid:!1,base:{kind:"git-tree",value:String(e||"<missing>")},candidate:{kind:"candidate-tree",value:"<unavailable>"},supportedSmellIds:[..._],touchedPaths:[],changes:[],baseFindingCount:0,candidateFindingCount:0,historicalResidualCount:0,error:t}}function te({root:e,config:t,configPath:s="ark.config.json",baseRef:n,ts:i,createdPathsOnly:a=!1,missingBase:r="fail-closed"}){const o=r==="skip";if(typeof n!="string"||!n.trim())return N(n,o?"design-delta skipped: no resolvable Git base ref.":"--fail-on-new-smells requires --base-ref <git-ref>.");try{if(n.startsWith("-"))throw new Error('base ref must not start with "-".');const l=M(e,["rev-parse","--verify",`${n}^{commit}`],"base ref is unresolvable"),u=M(e,["rev-parse","--verify",`${l}^{tree}`],"base tree is unresolvable"),p=h(w.isAbsolute(s)?w.relative(e,s):s);if(!p||p.startsWith("../"))throw new Error("base config path must be inside the project root.");const f=JSON.parse(R(e,l,p)),m=A(e,["ls-tree","-r","--name-only","-z",l],"base tree listing failed").filter(y=>b.test(y)&&!y.endsWith(".d.ts")),v=m.filter(y=>T(e,y,f?.layers??[])).map(y=>({path:y,content:R(e,l,y)})),S=F(e,t),d=[...A(e,["diff","--name-only","-z",l,"--"],"candidate diff failed"),...A(e,["ls-files","--others","--exclude-standard","-z"],"untracked-file scan failed")],c=I(e),g=E({root:e,config:f,records:v,ts:i,goldenPattern:c}),x=E({root:e,config:t,records:S,ts:i,goldenPattern:c});return L({mode:"git-base",baseIdentity:{kind:"git-tree",value:u,commit:l},candidateIdentity:{kind:"candidate-tree",value:C(t,S)},touchedPaths:d,baseFindings:g,candidateFindings:x,createdPathsOnly:!!a,baseTreePaths:m})}catch(l){return N(n,l instanceof Error?l.message:String(l))}}function ne(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
3
|
Next action: ${t.repairHint}`)].join(`
|
|
4
|
-
`):`Design delta unavailable: ${e?.error||"unknown error"}`}function
|
|
4
|
+
`):`Design delta unavailable: ${e?.error||"unknown error"}`}function ue({enabled:e,createdPathsOnly:t,missingBase:s,...n}){const i=s==="skip",a=e?te({...n,createdPathsOnly:!!t,missingBase:i?"skip":"fail-closed"}):null,r=a&&i&&!a.complete?null:a;return{result:r,combineEdges:({activeViolationCount:o,strictConfig:l,strictWarningCount:u,policyValid:p})=>{const f=o===0&&(!l||u===0)&&p;return{edgeValid:f,observedOk:f&&(r?.valid??!0)}},exitCode:o=>r&&!r.complete?2:o,failureText:()=>r&&!r.valid?ne(r):null}}function pe(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{P as DESIGN_DELTA_SCHEMA_VERSION,_ as DESIGN_DELTA_SUPPORTED_SMELLS,E as analyzeDesignFindings,ue as createDesignDeltaCheck,pe as designDeltaDoctorLines,te as evaluateGitDesignDelta,de as evaluateWriteDesignDelta,ne as formatDesignDeltaBlock};
|
|
@@ -483,7 +483,7 @@ export function summarizeDesignFitness(smells, ctx = {}) {
|
|
|
483
483
|
smellCount: Array.isArray(smells) ? smells.length : 0,
|
|
484
484
|
ids: (smells || []).map((s) => s.id),
|
|
485
485
|
label: designWeak
|
|
486
|
-
? `${operatingModeTitle(ctx.operatingMode, true)} — import rules check out; leftover design work remains
|
|
486
|
+
? `${operatingModeTitle(ctx.operatingMode, true)} — import rules check out; leftover design work remains`
|
|
487
487
|
: smells.length > 0
|
|
488
488
|
? 'Design smells present alongside open import-rule debt'
|
|
489
489
|
: 'No deterministic design smells detected',
|
|
@@ -32,7 +32,7 @@ function entry(ruleId, category, title, why, fix, extras) {
|
|
|
32
32
|
*/
|
|
33
33
|
export const DIAGNOSTIC_CATALOG = Object.freeze([
|
|
34
34
|
// ── layer / graph ────────────────────────────────────────────────────────
|
|
35
|
-
entry('LAYER_IMPORT_VIOLATION', 'layer', 'Layer import not allowed', 'A module import (or re-export) crosses a layer edge that ark.config.json does not allow. The architecture contract forbids that dependency direction so outer infrastructure cannot leak into pure or inner layers.', '
|
|
35
|
+
entry('LAYER_IMPORT_VIOLATION', 'layer', 'Layer import not allowed', 'A module import (or re-export) crosses a layer edge that ark.config.json does not allow. The architecture contract forbids that dependency direction so outer infrastructure cannot leak into pure or inner layers.', 'Branch by import kind: constants/types/pure → adopt into DomainModel or SharedKernel (do not invent a port); kernel/events/bootstrap from Persistence → inject a port or move the map to SharedTypes (Persistence must not emit); define a port only when the target is a real use-case. Type-only edges use `import type`. Then preflight again. Do not weaken the layer rule without a hash-bound policy acknowledgement.'),
|
|
36
36
|
entry('LAYER_INTENT_REFERENCE_VIOLATION', 'layer', 'Intent referenced across a blocked layer edge', 'A string intent (or intent-like reference) names a layer that the file’s layer may not reach under the contract rules — the same plane as import edges, for event/intent coupling.', 'Reference that intent from a layer allowed to know about it (usually an adapter or application layer), or relocate the reference — then preflight again.'),
|
|
37
37
|
entry('LAYER_REFERENCE_VIOLATION', 'layer', 'Layer reference blocked (snippet / AI gate)', 'Snippet analysis found an intent or string reference that would couple layers in a direction the architecture profile forbids.', 'Move the reference to an allowed layer or introduce a port/event boundary, then re-run the snippet gate.'),
|
|
38
38
|
entry('CIRCULAR_DEPENDENCY', 'layer', 'Dependency cycle', 'Two or more modules import each other in a loop. Cycles make ownership unclear and break stable layer direction.', 'Extract the shared dependency into a third module, invert one edge behind a port, or merge units that are truly one — then preflight again.'),
|
|
@@ -55,7 +55,7 @@ export const DIAGNOSTIC_CATALOG = Object.freeze([
|
|
|
55
55
|
entry('ARKRULE_STRUCTURE', 'arkrules', 'ArkRule structure sensor failed', 'An opt-in ArkRules structure sensor (private state, factory shape, event publish, …) failed on a governed file for a declared arkruleId.', 'Restore the declared structure for the ArkRule (see arkruleSource), then preflight again. Do not demote the rule without a hash-bound policy acknowledgement.'),
|
|
56
56
|
entry('ARKRULE_INVARIANT', 'arkrules', 'ArkRule invariant failed', 'Reserved / remediation-recognized code for invariant-plane failures bound to an ArkRule id (coverage path also emits INVARIANT_UNCOVERED).', 'Fix the invariant for the ArkRule declared in arkrules/<Layer>.json, then preflight again. Do not demote without acknowledgement.'),
|
|
57
57
|
entry('ARKRULE_SCOPE_EMPTY', 'arkrules', 'ArkRule appliesTo matched zero files', 'An ArkRule’s appliesTo globs matched no governed files — the rule cannot observe what it claims to protect.', 'Fix appliesTo globs so they match governed files, or remove the rule. Enforced empty scope fails; advisory empty scope warns.', { oftenAdvisory: true }),
|
|
58
|
-
entry('INVARIANT_UNCOVERED', 'arkrules', 'Invariant without coverage evidence', 'An ArkRules invariant is under contract but no covering test title or declared symbol evidence was found (or coverage is partial).', 'Add a test title or declared symbol covering the arkruleId, then preflight again. Missing test globs report partial — never fake green.'),
|
|
58
|
+
entry('INVARIANT_UNCOVERED', 'arkrules', 'Invariant without coverage evidence', 'An ArkRules invariant is under contract but no covering test title or declared symbol evidence was found (or coverage is partial). Kind is never-had-tests (adopt residual) vs tests-disappeared (suite exists).', 'Add a test title or declared symbol covering the arkruleId, then preflight again. Treat never-had-tests as adopt residual; treat tests-disappeared as a regression. Missing test globs report partial — never fake green.'),
|
|
59
59
|
// ── atomic preflight / change set ────────────────────────────────────────
|
|
60
60
|
entry('INVALID_CHANGE_PATH', 'preflight', 'Unsafe change path', 'A change set entry is not a safe, non-empty project-relative path (absolute, escape, empty, or NUL).', 'Use canonical project-relative paths only in the atomic change set, then preflight again.'),
|
|
61
61
|
entry('DUPLICATE_CHANGE_PATH', 'preflight', 'Duplicate path in change set', 'The atomic change set lists more than one operation for the same path.', 'Collapse to one create/update/delete per path, then preflight again.'),
|
|
@@ -67,9 +67,10 @@ export const DIAGNOSTIC_CATALOG = Object.freeze([
|
|
|
67
67
|
entry('CANDIDATE_CONTENT_HASH_MISMATCH', 'preflight', 'Candidate content hash mismatch', 'The candidate file content hash does not match the hash expected for the declared change.', 'Rebuild candidate facts from the exact proposed content, then preflight again.'),
|
|
68
68
|
entry('UNDECLARED_CANDIDATE_CHANGE', 'preflight', 'Undeclared candidate change', 'Candidate facts differ from base for a path that was not listed in the explicit change set.', 'Declare every path that changes in the atomic change set, then preflight again.'),
|
|
69
69
|
entry('ATOMIC_PREFLIGHT_UNAVAILABLE', 'preflight', 'Atomic preflight unavailable', 'The host/MCP path could not run the atomic preflight engine (missing facts, incomplete setup, or unsupported mode).', 'Use resolved-candidate facts / ark_prepare_change with a complete batch, or fall back to ark-check on disk. Do not treat missing preflight as green.'),
|
|
70
|
-
entry('DESIGN_SMELL_REGRESSION', 'preflight', 'Design smell regression on base-relative ratchet', 'Compared to the base ref, the candidate introduces or worsens a blocking design-smell class
|
|
70
|
+
entry('DESIGN_SMELL_REGRESSION', 'preflight', 'Design smell regression on base-relative ratchet', 'Compared to the base ref, the candidate introduces a created-path domain-logic-in-ui file under --strict-merge, or introduces or worsens a blocking design-smell class under --fail-on-new-smells.', 'Move the new UI business rule out of the created file (or revert a --fail-on-new-smells regression), then re-run with the same base ref.'),
|
|
71
71
|
// ── analysis completeness / host ─────────────────────────────────────────
|
|
72
|
-
entry('ANALYSIS_PARSE_INCOMPLETE', 'analysis', 'Parse incomplete', 'Governed source could not be fully parsed;
|
|
72
|
+
entry('ANALYSIS_PARSE_INCOMPLETE', 'analysis', 'Parse incomplete', 'Governed source could not be fully parsed; evidence includes the TypeScript diagnostic (line + message). Incremental mid-edit parse is normal for agents. Contract exclude paths skip the write hook.', 'Finish the source or fix the reported syntax error, then re-run `npx arkgate-check`. The write hook does not deny solely on mid-edit parse. Partial never means pass.'),
|
|
73
|
+
entry('LEXICAL_EVIDENCE_INCOMPLETE', 'analysis', 'Lexical evidence incomplete', 'Single-file validation cannot prove project module resolution. The write hook is already the verdict.', 'Re-run `npx arkgate-check --root . --config ark.config.json`, or treat the hook deny as final. Do not call ark_prepare_change from a hook deny.'),
|
|
73
74
|
entry('ANALYSIS_HOST_UNAVAILABLE', 'analysis', 'Analysis host unavailable', 'No usable TypeScript / analysis host was available for this invocation.', 'Install a supported TypeScript version visible to the project, then re-run. Unavailable analysis is fail-closed.'),
|
|
74
75
|
entry('ADAPTER_NOT_ALLOWED_FOR_PORT', 'adapter', 'Adapter not allowed for port', 'Runtime/port wiring selected an adapter implementation that the architecture profile does not allow for that port.', 'Bind an allowed adapter for the port, or adjust the profile with an explicit policy decision — then re-run.'),
|
|
75
76
|
// ── AI snippet gate policy surface ───────────────────────────────────────
|
|
@@ -88,7 +89,7 @@ export const DIAGNOSTIC_CATALOG = Object.freeze([
|
|
|
88
89
|
entry('CONFIG_INVALID_FORBIDDEN_GLOBALS', 'config', 'Invalid forbiddenGlobals', 'A layer’s forbiddenGlobals is not an array of strings; the entry is ignored.', 'Use an array of strings (e.g. ["fetch", "Date.now"]).', { oftenAdvisory: true }),
|
|
89
90
|
entry('CONFIG_LAYER_WITHOUT_PATTERNS', 'config', 'Layer without patterns', 'A named layer has no file patterns and will never classify files.', 'Add patterns globs that match the layer’s source tree.', { oftenAdvisory: true }),
|
|
90
91
|
entry('CONFIG_INVALID_LAYER_PATTERN', 'config', 'Invalid layer pattern', 'A layer pattern is not a valid glob / failed to compile.', 'Fix the pattern syntax for that layer.', { oftenAdvisory: true }),
|
|
91
|
-
entry('CONFIG_LAYER_PATTERN_NO_MATCHES', 'config', 'Layer pattern matched no files', 'A layer pattern matched zero included files (often a typo or include mismatch).', 'Adjust the pattern or include roots
|
|
92
|
+
entry('CONFIG_LAYER_PATTERN_NO_MATCHES', 'config', 'Layer pattern matched no files', 'A layer pattern matched zero included files (often a typo or include mismatch). Reserved/allowEmpty houses do not emit this.', 'Adjust the pattern or include roots, or mark the layer reserved/allowEmpty if the glob is a future house.', { oftenAdvisory: true }),
|
|
92
93
|
entry('CONFIG_DUPLICATE_LAYER', 'config', 'Duplicate layer name', 'The same layer name appears more than once in configuration.', 'Rename or merge duplicate layer entries.', { oftenAdvisory: true }),
|
|
93
94
|
entry('CONFIG_RULE_UNKNOWN_FROM_LAYER', 'config', 'Rule unknown from layer', 'A dependency rule references a source layer name that is not declared.', 'Fix the rule’s from field to a declared layer name.', { oftenAdvisory: true }),
|
|
94
95
|
entry('CONFIG_RULE_UNKNOWN_TO_LAYER', 'config', 'Rule unknown to layer', 'A dependency rule references a target layer name that is not declared.', 'Fix the rule’s to field to a declared layer name.', { oftenAdvisory: true }),
|
|
@@ -73,9 +73,9 @@ export function printDoctorAdvisories(advisories, io) {
|
|
|
73
73
|
printParseHealthSection(advisories.parseHealth, io);
|
|
74
74
|
printGraphBlindSection(advisories.graphBlindSpots, io);
|
|
75
75
|
const nudge = advisories.stewardNudge;
|
|
76
|
-
if ((nudge?.needsStewards || nudge?.drift) && nudge.ask) {
|
|
76
|
+
if ((nudge?.needsStewards || nudge?.drift || nudge?.emptyStewardsPastGrace) && nudge.ask) {
|
|
77
77
|
console.log('');
|
|
78
|
-
console.log(io.color.bold('Stewards
|
|
78
|
+
console.log(io.color.bold('Stewards'));
|
|
79
79
|
io.line(io.warn, nudge.ask);
|
|
80
80
|
if (nudge.nextAction) io.line(' ', io.color.dim(`Next: ${nudge.nextAction}`));
|
|
81
81
|
}
|
|
@@ -6,9 +6,32 @@ import { arkCommand } from '../ark-shared.mjs';
|
|
|
6
6
|
import { skillGapsForActiveHost } from './agent-gates.mjs';
|
|
7
7
|
import { agentHomeConcernIsActive, agentHomeRefreshCommand } from './agent-homes.mjs';
|
|
8
8
|
import { mergePostGreenTopActions } from './post-green-path.mjs';
|
|
9
|
+
import { ADOPTED_NOT, NOT_ADOPTED_NEXT_ACTION } from './adoption-stance.mjs';
|
|
9
10
|
|
|
10
11
|
export function collectDoctorNextActions(ctx) {
|
|
11
12
|
const actions = [];
|
|
13
|
+
const gatesInstalled = Array.isArray(ctx.gatesMissing) && ctx.gatesMissing.length === 0;
|
|
14
|
+
const planAEmpty = !ctx.activeCount;
|
|
15
|
+
const notAdopted = ctx.adopted !== 'required-merge' && ctx.adopted !== 'advisory-only-acked';
|
|
16
|
+
if (notAdopted || ctx.adopted === ADOPTED_NOT || ctx.adopted == null) {
|
|
17
|
+
actions.push(ctx.notAdoptedNextAction || NOT_ADOPTED_NEXT_ACTION);
|
|
18
|
+
}
|
|
19
|
+
const nudge = ctx.stewardNudge;
|
|
20
|
+
if (
|
|
21
|
+
nudge &&
|
|
22
|
+
(nudge.needsStewards || nudge.drift || nudge.emptyStewardsPastGrace) &&
|
|
23
|
+
nudge.nextAction
|
|
24
|
+
) {
|
|
25
|
+
actions.push(nudge.nextAction);
|
|
26
|
+
}
|
|
27
|
+
const enforceEmptyPlan =
|
|
28
|
+
ctx.operatingMode === 'enforce' && planAEmpty && gatesInstalled && !notAdopted;
|
|
29
|
+
if (enforceEmptyPlan) {
|
|
30
|
+
actions.push(
|
|
31
|
+
ctx.postGreenPath?.action ||
|
|
32
|
+
'/ark-explore, then one small refactor with /ark-autopilot and your OK'
|
|
33
|
+
);
|
|
34
|
+
}
|
|
12
35
|
if (!ctx.analysisComplete) actions.push('restore complete analysis, then rerun ark-check --doctor');
|
|
13
36
|
if (ctx.designSmells.length > 0 && ctx.postGreenPath) actions.push(ctx.postGreenPath.action);
|
|
14
37
|
if (ctx.coverageHonesty.greenIsNotEnforcement && ctx.coverageHonesty.worseThanNoGate) {
|
|
@@ -31,8 +54,8 @@ export function collectDoctorNextActions(ctx) {
|
|
|
31
54
|
`resolve the non-baselined violations — see the classified plan (${arkCommand(ctx.root, 'ark-check', '--plan')}), then /ark-autopilot`
|
|
32
55
|
);
|
|
33
56
|
}
|
|
34
|
-
if (ctx.writePath?.gap?.fix) actions.push(ctx.writePath.gap.fix);
|
|
35
|
-
if (ctx.gatesMissing.length > 0) {
|
|
57
|
+
if (ctx.writePath?.gap?.fix && !gatesInstalled) actions.push(ctx.writePath.gap.fix);
|
|
58
|
+
if (!gatesInstalled && ctx.gatesMissing.length > 0) {
|
|
36
59
|
actions.push(`install gates (${arkCommand(ctx.root, 'ark-check', '--install-agent-gates')})`);
|
|
37
60
|
}
|
|
38
61
|
const humanSkillGaps = skillGapsForActiveHost(ctx.skillGaps);
|
|
@@ -45,10 +68,12 @@ export function collectDoctorNextActions(ctx) {
|
|
|
45
68
|
if (legacyCodex) {
|
|
46
69
|
actions.push('install Codex SKILL.md catalog (--install-agent-gates --skills-only --tools codex --force)');
|
|
47
70
|
}
|
|
48
|
-
if (remMiss
|
|
49
|
-
actions.push('
|
|
71
|
+
if (remMiss > 0) {
|
|
72
|
+
actions.push('install missing /ark-* skills (--install-agent-gates --skills-only --force)');
|
|
73
|
+
} else if (remStale > 0) {
|
|
74
|
+
actions.push('refresh stale /ark-* skills (--install-agent-gates --skills-only --force) — gates are installed, catalog is stale');
|
|
50
75
|
}
|
|
51
|
-
if (ctx.codexHomeGap && ctx.codexConcernActive) {
|
|
76
|
+
if (ctx.codexHomeGap && ctx.codexConcernActive && ctx.codexHomeGap.preferProject !== true) {
|
|
52
77
|
actions.push(
|
|
53
78
|
ctx.codexHomeGap.catalogMetadataInvalid
|
|
54
79
|
? 'repair invalid Codex home catalog metadata after verifying the newest installed version'
|
|
@@ -76,6 +101,7 @@ export function collectDoctorNextActions(ctx) {
|
|
|
76
101
|
);
|
|
77
102
|
}
|
|
78
103
|
for (const gap of ctx.adoption.gaps) {
|
|
104
|
+
if (gap.id === 'adoption-stance-missing') continue;
|
|
79
105
|
if (!gap.deferred) actions.push(gap.fix || gap.message);
|
|
80
106
|
}
|
|
81
107
|
if (ctx.safety && ctx.safetyHasEntries) {
|
|
@@ -88,5 +114,9 @@ export function collectDoctorNextActions(ctx) {
|
|
|
88
114
|
if (ctx.designFitness.designWeak && unique.length === 0 && ctx.postGreenPath) {
|
|
89
115
|
unique.push(ctx.postGreenPath.action);
|
|
90
116
|
}
|
|
117
|
+
if (notAdopted) {
|
|
118
|
+
const next = ctx.notAdoptedNextAction || NOT_ADOPTED_NEXT_ACTION;
|
|
119
|
+
return [next, ...unique.filter((a) => a !== next)];
|
|
120
|
+
}
|
|
91
121
|
return unique;
|
|
92
122
|
}
|