arkgate 4.5.0 → 4.5.5

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 (42) hide show
  1. package/CHANGELOG.md +26 -2
  2. package/README.md +6 -4
  3. package/bin/ark-check-runtime.mjs +4 -0
  4. package/bin/lib/adapter-contract.mjs +5 -5
  5. package/bin/lib/analysis-engine.mjs +1 -1
  6. package/bin/lib/ci-and-commands.mjs +5 -0
  7. package/bin/lib/deep-module-coach.mjs +177 -0
  8. package/bin/lib/deepening-coach.mjs +177 -0
  9. package/bin/lib/doctor-plan.mjs +14 -0
  10. package/bin/lib/html-report-advisories.mjs +33 -0
  11. package/bin/lib/html-report-depth.mjs +9 -0
  12. package/bin/lib/managed-upgrade.mjs +17 -0
  13. package/bin/lib/remediation.mjs +5 -5
  14. package/bin/lib/upgrade-whats-new.mjs +110 -0
  15. package/dist/eslint/index.cjs +1 -1
  16. package/dist/eslint/index.js +1 -1
  17. package/dist/index.cjs +19 -19
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.js +22 -22
  20. package/docs/README.md +5 -5
  21. package/docs/agent-guide.md +15 -0
  22. package/docs/brownfield-adoption.md +12 -0
  23. package/docs/package-surface.md +4 -3
  24. package/docs/product-voice.md +21 -0
  25. package/docs/use.md +3 -0
  26. package/package.json +1 -1
  27. package/server.json +2 -2
  28. package/templates/agent-skills/README.md +2 -2
  29. package/templates/agent-skills/ark-adopt/SKILL.md +13 -0
  30. package/templates/agent-skills/ark-explore/SKILL.md +21 -0
  31. package/templates/agent-skills/ark-fix/SKILL.md +7 -0
  32. package/templates/agent-skills/ark-loop/SKILL.md +7 -0
  33. package/templates/agent-skills/ark-place/SKILL.md +7 -0
  34. package/templates/agent-skills/ark-think/SKILL.md +7 -0
  35. package/templates/agent-skills/ark-upgrade/SKILL.md +14 -0
  36. package/templates/skills/ark-adopt.md +13 -0
  37. package/templates/skills/ark-explore.md +21 -0
  38. package/templates/skills/ark-fix.md +7 -0
  39. package/templates/skills/ark-loop.md +7 -0
  40. package/templates/skills/ark-place.md +7 -0
  41. package/templates/skills/ark-think.md +7 -0
  42. package/templates/skills/ark-upgrade.md +14 -0
package/CHANGELOG.md CHANGED
@@ -5,14 +5,38 @@ in the immutable pre-2.0 archive linked below.
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 4.5.5 — 2026-08-11
9
+
10
+ **Patch** over **4.5.0**. Deep-module coach train plus upgrade **suggested improvements /
11
+ what’s new** so consumers know what to try after install. **No required config migration.**
12
+ No new skill names, sensors, or scores. Freezes held.
13
+
14
+ **Status: prepared** (implementation only; not yet on npm `latest` — remains `4.5.0` until
15
+ the publication checklist completes).
16
+
17
+ ### Added
18
+
19
+ - **Deep-module coach (advisory):** `ark-check --doctor` JSON `doctor.deepModuleCoach` + human
20
+ section + HTML `data-advisory="deepModuleCoach"`. Hot paths from bounded git history
21
+ (`unavailable` + empty when history missing — never invent). Deepening candidates projected
22
+ only from existing design smells / physical cohesion / pilot / residual compass evidence —
23
+ empty when no evidence. Always `notAScore: true`; never flips gate verdicts. Pure helper
24
+ `buildDeepeningCandidates` (+ CLI gen mirror).
25
+ - **Product voice + skills:** lexicon for depth / seam / deletion test / test-at-public-interface
26
+ / domain glossary / two-axis done. Existing skills (explore, think, place, fix, loop, adopt)
27
+ deepened; agent-skills 1:1. Compact router + agent-guide **two-axis done** (Enforce green ≠
28
+ feature done). Remediation port/inject `nextAction` prose includes test at the public interface.
29
+ - **Upgrade suggested improvements / what’s new:** `ark upgrade` JSON `whatsNew` + human block
30
+ lists concrete try/inspect actions (deep-module coach, improvement compass, session/status
31
+ honesty, two-axis done, self-service honesty). Always `notAScore`; never a gate input.
32
+
8
33
  ## 4.5.0 — 2026-08-10
9
34
 
10
35
  **Minor** over 4.4.0. Session control-plane honesty (status/MCP improvement compass with explicit
11
36
  modes), domain module fitness (split oversized pure modules), selective pure verification ratchet,
12
37
  self-service upgrade activation honesty, and a short session recipe in product docs. **No required
13
38
  config migration.** Codex remains advisory at write time. Skills and AGENTS projection never
14
- enforce. **Status: prepared in tree — not published on npm until OIDC publish + `npm view` succeed**
15
- (see `docs/releases/4.5.0.md`).
39
+ enforce. **Status: published** (`arkgate@4.5.0` on npm `latest`; see `docs/releases/4.5.0.md`).
16
40
 
17
41
  ### Added
18
42
 
package/README.md CHANGED
@@ -16,8 +16,9 @@ and makes sure a “green” check means something real.
16
16
 
17
17
  </div>
18
18
 
19
- > **ArkGate 4.4.0** is on npm `latest`. Tree is preparing **4.5.0** (session honesty + upgrade self-service) — see [4.5.0 notes](docs/releases/4.5.0.md) (Status: prepared until publish).
20
- > [4.5.0 notes](docs/releases/4.5.0.md) · [4.4.0](docs/releases/4.4.0.md) · [4.3.0](docs/releases/4.3.0.md) · [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md)
19
+ > **ArkGate 4.5.5** is **prepared** (deep-module coach + upgrade what’s new). npm `latest` remains
20
+ > **4.5.0** until publish. [4.5.5 notes](docs/releases/4.5.5.md) · [4.5.0](docs/releases/4.5.0.md) ·
21
+ > [4.4.0](docs/releases/4.4.0.md) · [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md)
21
22
 
22
23
  ---
23
24
 
@@ -208,8 +209,9 @@ for real systems. Details: [docs/production-hardening.md](docs/production-harden
208
209
  | Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
209
210
  | Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
210
211
  | Security | [SECURITY.md](SECURITY.md) |
211
- | Current release (4.4.0 on npm `latest`) | [docs/releases/4.4.0.md](docs/releases/4.4.0.md) · [CHANGELOG](CHANGELOG.md) |
212
- | Next prepare (4.5.0) | [docs/releases/4.5.0.md](docs/releases/4.5.0.md) (prepared — not published until npm verify) |
212
+ | Prepared (4.5.5) | [docs/releases/4.5.5.md](docs/releases/4.5.5.md) · [CHANGELOG](CHANGELOG.md) |
213
+ | Current published (4.5.0 on npm `latest`) | [docs/releases/4.5.0.md](docs/releases/4.5.0.md) |
214
+ | Prior (4.4.0) | [docs/releases/4.4.0.md](docs/releases/4.4.0.md) |
213
215
  | Prior (4.3.0) | [docs/releases/4.3.0.md](docs/releases/4.3.0.md) |
214
216
  | Prior (4.2.1) | [docs/releases/4.2.1.md](docs/releases/4.2.1.md) |
215
217
  | Previous (4.2.0) | [docs/releases/4.2.0.md](docs/releases/4.2.0.md) |
@@ -1644,6 +1644,10 @@ async function main() {
1644
1644
  ...(designDepth?.improvementCompass
1645
1645
  ? { improvementCompass: designDepth.improvementCompass }
1646
1646
  : {}),
1647
+ // Doctor parity: deep-module coach advisory (hot paths + deepening; notAScore).
1648
+ ...(designDepth?.deepModuleCoach
1649
+ ? { deepModuleCoach: designDepth.deepModuleCoach }
1650
+ : {}),
1647
1651
  },
1648
1652
  };
1649
1653
  const html = args.beginner
@@ -82,18 +82,18 @@ function nextActionForDiagnostic(ruleId, evidence, violation) {
82
82
  return 'Move the referenced type to a mutually allowed layer, use `import type`, then preflight again.';
83
83
  }
84
84
  if (violation.peerIsolation === true) {
85
- return 'Extract the shared dependency to a shared layer, then preflight again.';
85
+ return 'Extract the shared dependency to a shared layer, test at the public interface, then preflight again.';
86
86
  }
87
- return `Define a port in ${evidence.fromLayer ?? 'the source layer'}, inject the ${evidence.toLayer ?? 'outer-layer'} implementation, then preflight again.`;
87
+ return `Define a port in ${evidence.fromLayer ?? 'the source layer'}, inject the ${evidence.toLayer ?? 'outer-layer'} implementation, test at the public interface, then preflight again.`;
88
88
  }
89
89
  if (ruleId === 'FORBIDDEN_GLOBAL') {
90
- return `Inject ${evidence.target ?? 'the capability'} through a port, then preflight again.`;
90
+ return `Inject ${evidence.target ?? 'the capability'} through a port, test at the public interface, then preflight again.`;
91
91
  }
92
92
  if (ruleId === 'CAPABILITY_VIOLATION') {
93
- return `Define a ${text(violation.capability) ?? 'capability'} port in ${evidence.fromLayer ?? 'the walled layer'}, bind the implementation outside it, then preflight again.`;
93
+ return `Define a ${text(violation.capability) ?? 'capability'} port in ${evidence.fromLayer ?? 'the walled layer'}, bind the implementation outside it, test at the public interface, then preflight again.`;
94
94
  }
95
95
  if (ruleId === 'CIRCULAR_DEPENDENCY') {
96
- return 'Extract the shared dependency into a third module, then preflight again.';
96
+ return 'Extract the shared dependency into a third module, test at the public interface, then preflight again.';
97
97
  }
98
98
  if (ruleId === 'RAW_EVENT_PUBLISH')
99
99
  return 'Publish through a registered intent creator, then run Ark again.';
@@ -7,4 +7,4 @@ ${n.map(r=>`- ${r.path}: ${r.message}`).join(`
7
7
  `)}`),this.name="ArkConfigValidationError",this.source=t,this.issues=n}};function Mt(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)}function xe(e,t){return/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(t)?`${e}.${t}`:`${e}[${JSON.stringify(t)}]`}function le(e){return e===null?"null":Array.isArray(e)?"array":typeof e}function Vn(e,t){let n="#/$defs/";if(e.startsWith(n))return t.$defs[e.slice(n.length)]}function Ae(e,t,n,r,s){if(t.$ref){let a=Vn(t.$ref,r);if(!a){s.push({path:n,message:`schema reference ${t.$ref} cannot be resolved`});return}Ae(e,a,n,r,s);return}if(t.const!==void 0&&!Object.is(e,t.const)){s.push({path:n,message:`must equal ${JSON.stringify(t.const)}`});return}if(t.enum&&!t.enum.some(a=>Object.is(a,e))){s.push({path:n,message:`must be one of ${t.enum.map(String).join(", ")}`});return}if(t.type==="object"){if(!Mt(e)){s.push({path:n,message:`must be an object; received ${le(e)}`});return}let a=t.properties??{};for(let i of t.required??[])e[i]===void 0&&s.push({path:xe(n,i),message:"is required"});if(t.additionalProperties===!1)for(let i of Object.keys(e))i in a||s.push({path:xe(n,i),message:"unknown field"});else if(t.additionalProperties!==void 0&&t.additionalProperties!==!0&&typeof t.additionalProperties=="object"){let i=t.additionalProperties;for(let d of Object.keys(e))d in a||Ae(e[d],i,xe(n,d),r,s)}for(let[i,d]of Object.entries(a))e[i]!==void 0&&Ae(e[i],d,xe(n,i),r,s);return}if(t.type==="array"){if(!Array.isArray(e)){s.push({path:n,message:`must be an array; received ${le(e)}`});return}if(t.minItems!==void 0&&e.length<t.minItems&&s.push({path:n,message:`must contain at least ${t.minItems} item(s)`}),t.uniqueItems){let a=e.map(i=>JSON.stringify(i));new Set(a).size!==a.length&&s.push({path:n,message:"must not contain duplicate items"})}t.items&&e.forEach((a,i)=>Ae(a,t.items,`${n}[${i}]`,r,s));return}if(t.type==="string"){if(typeof e!="string"){s.push({path:n,message:`must be a string; received ${le(e)}`});return}t.minLength!==void 0&&e.length<t.minLength&&s.push({path:n,message:`must contain at least ${t.minLength} character(s)`});return}if(t.type==="boolean"){typeof e!="boolean"&&s.push({path:n,message:`must be a boolean; received ${le(e)}`});return}if(t.type==="integer"){if(!Number.isInteger(e)){s.push({path:n,message:`must be an integer; received ${le(e)}`});return}t.minimum!==void 0&&e<t.minimum&&s.push({path:n,message:`must be at least ${t.minimum}`})}}function Un(e){return{...e,$schema:e.$schema===void 0?nt:e.$schema,schemaVersion:e.schemaVersion===void 0?"1.1":e.schemaVersion,include:e.include===void 0?["src"]:e.include,layers:e.layers===void 0?[]:e.layers,rules:e.rules===void 0?Nt.map(t=>({...t})):e.rules}}function Gn(){let e=new Set(["1.1"]);for(let t of rt)t.from!=="unversioned"&&e.add(t.from),e.add(t.to);return e}function qn(e,t="ark.config.json"){if(!Mt(e))throw new G(t,[{path:"$",message:`must be an object; received ${le(e)}`}]);let n=Gn(),r=e.schemaVersion===void 0?"unversioned":typeof e.schemaVersion=="string"?e.schemaVersion:null;if(r===null)throw new G(t,[{path:"$.schemaVersion",message:`unsupported version ${JSON.stringify(e.schemaVersion)}; expected 1.1`}]);if(r!=="unversioned"&&!n.has(r))throw new G(t,[{path:"$.schemaVersion",message:`unsupported version ${JSON.stringify(r)}; expected 1.1`}]);let s=r,a={...e},i=0;for(;s!=="1.1"&&i<rt.length+1;){i+=1;let p=rt.find(b=>b.from===s);if(!p)throw new G(t,[{path:"$.schemaVersion",message:`unsupported version ${JSON.stringify(s)}; expected 1.1`}]);s=p.to,a.schemaVersion=s}if(s!=="1.1")throw new G(t,[{path:"$.schemaVersion",message:`unsupported version ${JSON.stringify(r)}; expected 1.1`}]);let d=r==="unversioned"?"unversioned":r==="1.0"?"1.0":null;return{candidate:Un(a),migratedFrom:d}}function st(e,t="ark.config.json"){let{candidate:n,migratedFrom:r}=qn(e,t),s=[];if(Ae(n,Lt,"$",Lt,s),s.length>0)throw new G(t,s);return{config:n,migratedFrom:r}}function Ht(e,t="ark.config.json"){let n;try{n=JSON.parse(e)}catch(r){throw new G(t,[{path:"$",message:`invalid JSON: ${r instanceof Error?r.message:String(r)}`}])}return st(n,t)}function Dt(e){return{config:e.config,arkRules:{schemaVersion:e.arkRules.schemaVersion,structure:e.arkRules.structure.map(t=>({id:t.id,sensor:t.sensor,mode:t.mode,appliesTo:t.appliesTo??null,description:t.description??null,provenance:t.provenance})),invariants:e.arkRules.invariants.map(t=>({id:t.id,description:t.description,aggregate:t.aggregate??null,coverage:t.coverage??null,mode:t.mode,appliesTo:t.appliesTo??null,provenance:t.provenance}))}}}var Ut="1.0";function I(e,t){e.push({id:`${t.classification}:${t.path}:${t.kind}`,path:t.path,classification:t.classification,message:t.message,...t.classification==="weakening"||t.classification==="judgment-required"?{nextAction:`Restore the previous protection at ${t.path}, then run ArkGate again.`}:{},...t.before===void 0?{}:{before:t.before},...t.after===void 0?{}:{after:t.after}})}function T(e){return[...new Set(e??[])].sort()}function X(e,t,n,r,s){let a=T(n),i=T(r),d=new Set(a),p=new Set(i),b=i.filter(h=>!d.has(h)),A=a.filter(h=>!p.has(h));b.length===0&&A.length===0||(b.length>0&&I(e,{kind:"added",path:t,classification:s.added,message:s.addedMessage,before:a,after:i}),A.length>0&&I(e,{kind:"removed",path:t,classification:s.removed,message:s.removedMessage,before:a,after:i}))}function Oe(e,t,n,r,s,a,i){if(n===r)return;I(e,{kind:r?"enabled":"disabled",path:t,classification:r?s:s==="strengthening"?"weakening":"strengthening",message:r?a:i,before:n,after:r})}function Pe(e,t){let n=new Map,r=new Set;for(let s of e){let a=t(s);n.has(a)?r.add(a):n.set(a,s)}return{values:n,duplicates:[...r].sort()}}function zn(e,t,n){let r=Pe(t,a=>a.name),s=Pe(n,a=>a.name);(r.duplicates.length>0||s.duplicates.length>0)&&I(e,{kind:"duplicate-layer",path:"$.layers",classification:"judgment-required",message:"Duplicate layer names make policy ownership ambiguous.",before:r.duplicates,after:s.duplicates});for(let a of[...new Set([...r.values.keys(),...s.values.keys()])].sort()){let i=r.values.get(a),d=s.values.get(a),p=`$.layers[${a}]`;if(!i&&d){I(e,{kind:"layer-added",path:p,classification:"judgment-required",message:"A layer was added; verify overlap, ownership, and rule coverage.",after:d});continue}if(i&&!d){I(e,{kind:"layer-removed",path:p,classification:"weakening",message:"Removing a layer can leave its source paths ungoverned.",before:i});continue}if(!i||!d)continue;X(e,`${p}.patterns`,i.patterns,d.patterns,{added:"strengthening",removed:"weakening",addedMessage:"Additional paths are governed by this layer.",removedMessage:"Paths were removed from this layer and may become ungoverned."}),X(e,`${p}.exclude`,i.exclude,d.exclude,{added:"weakening",removed:"strengthening",addedMessage:"Additional paths are excluded from this layer.",removedMessage:"Fewer paths are excluded from this layer."});let b=Se(i),A=Se(d);X(e,`${p}.forbiddenGlobals`,b.rawGlobals,A.rawGlobals,{added:"strengthening",removed:"weakening",addedMessage:"Additional forbidden globals are enforced in this layer.",removedMessage:"A forbidden-global protection was removed from this layer."}),X(e,`${p}.capabilities`,b.atoms,A.atoms,{added:"strengthening",removed:"weakening",addedMessage:"Additional ambient/import protection is enforced in this layer (coverage atoms).",removedMessage:"An ambient/import protection was lost from this layer (coverage atoms)."}),T(i.intentPrefixes).join("\0")!==T(d.intentPrefixes).join("\0")&&I(e,{kind:"intent-prefixes-changed",path:`${p}.intentPrefixes`,classification:"judgment-required",message:"Intent ownership changed and must be reviewed against publishers and consumers.",before:T(i.intentPrefixes),after:T(d.intentPrefixes)}),Oe(e,`${p}.mayImportInfrastructure`,i.mayImportInfrastructure===!0,d.mayImportInfrastructure===!0,"weakening","The layer may now import infrastructure directly.","Direct infrastructure imports are no longer allowed for this layer."),Oe(e,`${p}.optional`,i.optional===!0,d.optional===!0,"weakening","The layer is now optional and can be absent without a strict warning.","The layer is now required when its contract is active.")}}function Bn(e,t,n){let r=i=>`${i.from}->${i.to}`,s=Pe(t,r),a=Pe(n,r);(s.duplicates.length>0||a.duplicates.length>0)&&I(e,{kind:"duplicate-rule",path:"$.rules",classification:"judgment-required",message:"Duplicate rule edges make the effective verdict order-dependent.",before:s.duplicates,after:a.duplicates});for(let i of[...new Set([...s.values.keys(),...a.values.keys()])].sort()){let d=s.values.get(i),p=a.values.get(i),b=`$.rules[${i}]`;if(!d&&p){p.allowed===!1&&I(e,{kind:"deny-added",path:b,classification:"strengthening",message:"A denied dependency edge was added.",after:p});continue}if(d&&!p){d.allowed===!1&&I(e,{kind:"deny-removed",path:b,classification:"weakening",message:"A denied dependency edge was removed.",before:d});continue}if(!d||!p)continue;d.allowed!==p.allowed&&I(e,{kind:p.allowed?"deny-disabled":"deny-enabled",path:`${b}.allowed`,classification:p.allowed?"weakening":"strengthening",message:p.allowed?"A previously denied dependency edge is now allowed.":"A dependency edge is now denied.",before:d.allowed,after:p.allowed});let A=d.peerIsolation===!0,h=p.peerIsolation===!0;if(A!==h){let o=d.from===d.to&&p.from===p.to;I(e,{kind:h?"peer-isolation-enabled":"peer-isolation-disabled",path:`${b}.peerIsolation`,classification:o?h?"strengthening":"weakening":"judgment-required",message:o?h?"Cross-slice dependencies inside this layer are now denied.":"Cross-slice dependencies inside this layer are no longer denied.":"Changing peer isolation on a cross-layer edge changes the denial scope.",before:A,after:h})}T(d.sliceFolders).join("\0")!==T(p.sliceFolders).join("\0")&&I(e,{kind:"slice-folders-changed",path:`${b}.sliceFolders`,classification:"judgment-required",message:"Slice ownership folders changed and can reclassify existing dependencies.",before:T(d.sliceFolders),after:T(p.sliceFolders)})}}function Kn(e,t,n){let r=t.safety??{},s=n.safety??{};for(let a of["maxTsSuppressions","maxAnyCasts"]){let i=r[a]??0,d=s[a]??0;i!==d&&I(e,{kind:d>i?"threshold-raised":"threshold-lowered",path:`$.safety.${a}`,classification:d>i?"weakening":"strengthening",message:d>i?"The safety threshold allows more violations.":"The safety threshold allows fewer violations.",before:i,after:d})}for(let a of["allowInMemory","allowDisabledPeerIsolation"])Oe(e,`$.safety.${a}`,r[a]===!0,s[a]===!0,"weakening","A safety exception was enabled.","A safety exception was disabled.")}function Yn(e){return e.some(t=>t.classification==="weakening")?"weakening":e.some(t=>t.classification==="judgment-required")?"judgment-required":e.some(t=>t.classification==="strengthening")?"strengthening":"neutral"}function jt(e,t){return`${e}::${t}`}function Vt(e){let t=new Map,n=new Map;if(!e)return{structure:t,invariants:n};for(let r of e.structure)t.set(jt(r.provenance.layer,r.id),r);for(let r of e.invariants)n.set(jt(r.provenance.layer,r.id),r);return{structure:t,invariants:n}}function Wn(e,t,n,r,s,a){let i=new Map((a??[]).map(o=>[o.invariantId,o])),d=t.arkRules??{},p=n.arkRules??{},b=[...new Set([...Object.keys(d),...Object.keys(p)])].sort();for(let o of b){let l=d[o],c=p[o],f=`$.arkRules[${o}]`;if(l===void 0&&c!==void 0){I(e,{kind:"arkrules-ref-added",path:f,classification:"strengthening",message:`ArkRules reference for layer ${o} was added.`,after:c});continue}if(l!==void 0&&c===void 0){I(e,{kind:"arkrules-ref-removed",path:f,classification:"weakening",message:`ArkRules reference for layer ${o} was removed.`,before:l});continue}l!==c&&I(e,{kind:"arkrules-ref-path-changed",path:f,classification:"judgment-required",message:`ArkRules file path for layer ${o} changed; verify the effective rules still match intent.`,before:l,after:c})}let A=Vt(r),h=Vt(s);for(let o of[...new Set([...A.structure.keys(),...h.structure.keys()])].sort()){let l=A.structure.get(o),c=h.structure.get(o),f=`$.arkRules.structure[${o}]`;if(!l&&c){I(e,{kind:"arkrule-structure-added",path:f,classification:"strengthening",message:`Structure ArkRule ${c.id} was added (${c.mode}).`,after:c});continue}if(l&&!c){I(e,{kind:"arkrule-structure-removed",path:f,classification:"weakening",message:`Structure ArkRule ${l.id} was removed.`,before:l});continue}if(!(!l||!c)){if(l.mode!==c.mode){let y=l.mode==="advisory"&&c.mode==="enforced";I(e,{kind:y?"arkrule-promoted":"arkrule-demoted",path:`${f}.mode`,classification:y?"strengthening":"weakening",message:y?`Structure ArkRule ${c.id} was promoted to enforced.`:`Structure ArkRule ${c.id} was demoted to advisory.`,before:l.mode,after:c.mode})}l.sensor!==c.sensor&&I(e,{kind:"arkrule-sensor-changed",path:`${f}.sensor`,classification:"judgment-required",message:`Structure ArkRule ${c.id} changed sensor identity.`,before:l.sensor,after:c.sensor})}}for(let o of[...new Set([...A.invariants.keys(),...h.invariants.keys()])].sort()){let l=A.invariants.get(o),c=h.invariants.get(o),f=`$.arkRules.invariants[${o}]`;if(!l&&c){I(e,{kind:"arkrule-invariant-added",path:f,classification:"strengthening",message:`Invariant ${c.id} was added (${c.mode}).`,after:c});continue}if(l&&!c){I(e,{kind:"arkrule-invariant-removed",path:f,classification:"weakening",message:`Invariant ${l.id} was removed.`,before:l});continue}if(!(!l||!c)&&l.mode!==c.mode)if(l.mode==="advisory"&&c.mode==="enforced"){let R=i?.get(c.id),u=$t(R);u.ok?I(e,{kind:"arkrule-invariant-promoted",path:`${f}.mode`,classification:"strengthening",message:`Invariant ${c.id} was promoted to enforced (coverage evidence present).`,before:l.mode,after:c.mode}):I(e,{kind:"arkrule-invariant-promote-refused",path:`${f}.mode`,classification:"judgment-required",message:`Invariant ${c.id} cannot be promoted to enforced: ${u.reason}`,before:l.mode,after:c.mode})}else I(e,{kind:"arkrule-invariant-demoted",path:`${f}.mode`,classification:"weakening",message:`Invariant ${c.id} was demoted to advisory.`,before:l.mode,after:c.mode})}}function Gt(e,t,n){let r=[];X(r,"$.include",e.include,t.include,{added:"strengthening",removed:"weakening",addedMessage:"Additional project roots are governed.",removedMessage:"Project roots were removed from governance."}),X(r,"$.exclude",e.exclude,t.exclude,{added:"weakening",removed:"strengthening",addedMessage:"Additional project paths are excluded from governance.",removedMessage:"Fewer project paths are excluded from governance."}),X(r,"$.dynamicImportAllowlist",e.dynamicImportAllowlist,t.dynamicImportAllowlist,{added:"weakening",removed:"strengthening",addedMessage:"Additional files may use non-literal dynamic imports.",removedMessage:"Fewer files may use non-literal dynamic imports."}),Oe(r,"$.excludeGenerated",e.excludeGenerated!==!1,t.excludeGenerated!==!1,"weakening","Generated source is now excluded from governance.","Generated source is now governed.");let s={off:0,soft:1,"framework-soft":1,strict:2},a=e.cyclePolicy??"strict",i=t.cyclePolicy??"strict";if(a!==i){let d=s[i]===s[a]?"judgment-required":s[i]>s[a]?"strengthening":"weakening";I(r,{kind:"cycle-policy-changed",path:"$.cyclePolicy",classification:d,message:"The cycle enforcement level changed.",before:a,after:i})}return(e.frameworkOverlay??null)!==(t.frameworkOverlay??null)&&I(r,{kind:"framework-overlay-changed",path:"$.frameworkOverlay",classification:"judgment-required",message:"The framework overlay changed and may alter effective layer matching.",before:e.frameworkOverlay??null,after:t.frameworkOverlay??null}),zn(r,e.layers,t.layers),Bn(r,e.rules,t.rules),Kn(r,e,t),Wn(r,e,t,n?.baseArkRules,n?.candidateArkRules,n?.candidateInvariantCoverage),r.sort((d,p)=>d.path.localeCompare(p.path)||d.id.localeCompare(p.id)),{schemaVersion:Ut,classification:Yn(r),findings:r}}function qt(e,t){if(!e||e.schemaVersion!==Ut||typeof e.basePolicyHash!="string"||typeof e.candidatePolicyHash!="string"||typeof e.reason!="string"||!Array.isArray(e.findingIds)||e.findingIds.some(s=>typeof s!="string")||e.reason.trim().length===0||e.basePolicyHash!==t.basePolicyHash||e.candidatePolicyHash!==t.candidatePolicyHash)return!1;let n=T(e.findingIds),r=T(t.findingIds);return n.length===r.length&&n.every((s,a)=>s===r[a])}function Q(e){let t=[];for(let n of e.replace(/\\/g,"/").split("/"))!n||n==="."||(n===".."&&t.length>0&&t.at(-1)!==".."?t.pop():t.push(n));return t.join("/")}function zt(e){return e!==void 0&&/[A-Za-z_$]/.test(e)}function Fe(e){return e!==void 0&&/[A-Za-z0-9_$]/.test(e)}function P(e,t){for(;t<e.length&&/\s/.test(e[t]);)t+=1;return t}function Re(e,t){let n=e[t];if(n!=="'"&&n!=='"')return;let r=t,s="";for(t+=1;t<e.length;t+=1){let a=e[t];if(a===n)return{value:s,offset:r,excerpt:e.slice(r,t+1)};a==="\\"&&t+1<e.length?(s+=e[t+1],t+=1):s+=a}}function _(e,t,n){return e.startsWith(t,n)&&!Fe(e[n-1])&&!Fe(e[n+t.length])}function Bt(e,t){let n=P(e,t);if(e[n]!=="{")return!1;n+=1;let r=!1;for(;n<e.length;){if(n=P(e,n),e[n]==="}")return r;if(e[n]===","){n+=1;continue}if(!_(e,"type",n))return!1;let s=P(e,n+4);if(s>=e.length||e[s]===","||e[s]==="}"||_(e,"as",s)||!zt(e[s]))return!1;for(n=s;n<e.length&&Fe(e[n]);)n+=1;if(n=P(e,n),_(e,"as",n)){if(n=P(e,n+2),!zt(e[n]))return!1;for(;n<e.length&&Fe(e[n]);)n+=1}r=!0}return!1}function Jn(e,t){if(t=P(e,t+6),e[t]==="(")return Re(e,P(e,t+1));let n=!1;if(_(e,"type",t)){let s=P(e,t+4);e[s]!==","&&!_(e,"from",s)&&(n=!0)}else Bt(e,t)&&(n=!0);let r=Kt(e,t,!0);return r&&n?{...r,typeOnly:!0}:r}function Zn(e,t){t=t+6;let n=P(e,t),r=!1;if(_(e,"type",n)){let a=P(e,n+4);(e[a]==="{"||e[a]==="*")&&(r=!0)}else Bt(e,n)&&(r=!0);let s=Kt(e,t,!1);return s&&r?{...s,typeOnly:!0}:s}function Kt(e,t,n){for(;t<e.length;t+=1){if(e[t]===";")return;if(_(e,"from",t))return Re(e,P(e,t+4));if(n&&(e[t]==="'"||e[t]==='"'))return Re(e,t);if(t>0&&(_(e,"import",t)||_(e,"export",t)))return}}function Xn(e,t){for(t+=1;t<e.length;t+=1){let n=e[t];if(n==="\\")t+=1;else if(n==="`")return t}return e.length}function Qn(e,t){let n=t-1;for(;n>=0&&/\s/.test(e[n]);)n-=1;if(e[n]===".")return;let r=P(e,t+7);if(e[r]!=="(")return;r=P(e,r+1);let s=Re(e,r);return s?{...s,requireCall:!0}:void 0}function er(e){let t=[];for(let n=0;n<e.length;n+=1){let r=e[n];if(r==="/"&&e[n+1]==="/"){if(n=e.indexOf(`
8
8
  `,n+2),n<0)break;continue}if(r==="/"&&e[n+1]==="*"){let a=e.indexOf("*/",n+2);if(a<0)break;n=a+1;continue}if(r==="`"){n=Xn(e,n);continue}if(r==="'"||r==='"'){let a=Re(e,n);a&&(n=a.offset+a.excerpt.length-1);continue}let s=r==="i"&&_(e,"import",n)?Jn(e,n):r==="e"&&_(e,"export",n)?Zn(e,n):r==="r"&&_(e,"require",n)?Qn(e,n):void 0;s&&(t.push(s),n=s.offset+s.excerpt.length-1)}return t}function Yt(e,t){let n=[],r=[];for(let s of er(e.content)){let a=s.value;if(!a.startsWith(".")){if(s.typeOnly)continue;let b=pe(a);if(!b)continue;let A=e.content.slice(0,s.offset).split(`
9
9
  `).length;r.push({file:e.path,symbol:a,capability:b,evidence:{kind:"import",file:e.path,line:A,excerpt:s.excerpt}});continue}let i=e.content.slice(0,s.offset).split(`
10
- `).length,d={kind:"import",file:e.path,line:i,excerpt:s.excerpt},p=tr(e.path,a,t);n.push({from:e.path,specifier:a,to:p?.path??null,resolution:p?"resolved":"unresolved",fromLayer:e.layer,toLayer:p?.layer??null,evidence:d})}return{edges:n,capabilityUses:r}}function tr(e,t,n){let r=e.split("/");r.pop();for(let a of t.split("/"))a==="."||a===""||(a===".."?r.pop():r.push(a));let s=r.join("/");for(let a of[s,`${s}.ts`,`${s}.tsx`,`${s}.mts`,`${s}.cts`,`${s}/index.ts`,`${s}/index.tsx`]){let i=n.get(a);if(i)return i}}function Wt(e,t){let n=[];for(let r of e){if(!r.to||!r.fromLayer||!r.toLayer)continue;let s=ae(t.rules,r.fromLayer,r.toLayer,{fromPath:r.from,toPath:r.to,layers:t.layers});s&&n.push({ruleId:`layer-dependency:${s.from}->${s.to}`,message:s.message??`${s.from} must not depend on ${s.to}.`,edge:r,evidence:r.evidence})}return n}var nr={code:"LEXICAL_EVIDENCE_INCOMPLETE",message:"Lexical compatibility mode cannot prove parse status, TypeScript/package/symlink resolution, or symbol-aware source-policy and safety evidence. Use the resolved candidate facts APIs for an authoritative verdict."};function rr(e,t){return e.arkRules!==void 0&&Object.keys(e.arkRules).length>0||t.structure.length>0||t.invariants.length>0}function sr(e,t){return rr(e,t)?E(S(Dt({config:e,arkRules:t,warnings:[]}))):E(S(e))}function at(e,t,n){let r=typeof e=="string"?Ht(e,t):st(e,t),s=n?.arkRules??me();return{...r,arkRules:s,policyHash:sr(r.config,s)}}function ar(e){let t=at(e.baseConfig,e.baseSource??"base ark.config.json",{arkRules:e.baseArkRules}),n=at(e.candidateConfig,e.candidateSource??"candidate ark.config.json",{arkRules:e.candidateArkRules}),r=Gt(t.config,n.config,{baseArkRules:t.arkRules,candidateArkRules:n.arkRules,candidateInvariantCoverage:e.candidateInvariantCoverage}),s=r.findings.filter(d=>d.classification==="weakening"||d.classification==="judgment-required").map(d=>d.id).sort(),a=s.length>0,i=a&&qt(e.acknowledgement,{basePolicyHash:t.policyHash,candidatePolicyHash:n.policyHash,findingIds:s});return{schemaVersion:r.schemaVersion,basePolicyHash:t.policyHash,candidatePolicyHash:n.policyHash,classification:r.classification,findings:r.findings,blockingFindingIds:s,requiresAcknowledgement:a,acknowledged:i,valid:!a||i}}function Te(e){let t=e.files.map(h=>{let o=Q(h.path);return{path:o,content:h.content,contentHash:E(h.content),layer:V(o,e.contract.config.layers)??null}}).sort((h,o)=>h.path.localeCompare(o.path)),n=new Map(t.map(h=>[h.path,h])),r=[],s=[];for(let h of t){let o=Yt(h,n);r.push(...o.edges),s.push(...o.capabilityUses)}let a=Wt(r,e.contract.config),i=new Map(e.contract.config.layers.map(h=>[h.name,h])),d=new Map(e.contract.config.layers.map(h=>[h.name,new Set(se(h))]));for(let h of s){let o=n.get(h.file)?.layer;if(!o)continue;let l=i.get(o),c=fe(h.symbol,l?.forbiddenGlobals??[]);if(c){a.push({ruleId:"FORBIDDEN_GLOBAL",message:`${o} must not use module "${h.symbol}" because it is the import form of forbidden global "${c}".`,symbol:h.symbol,evidence:h.evidence});continue}d.get(o)?.has(h.capability)&&a.push({ruleId:"CAPABILITY_VIOLATION",message:`${o} denies the ${h.capability} capability; found import of "${h.symbol}".`,capability:h.capability,symbol:h.symbol,evidence:h.evidence})}let p=t.length===0?"complete":"partial",b=p==="complete"?[]:[{...nr}],A={schemaVersion:mt,policyHash:e.contract.policyHash,compilerOptionsHash:E(S(e.compilerOptions??{})),files:t,layers:e.contract.config.layers.map(h=>h.name),edges:r,capabilityUses:s,violations:a};return{mode:"lexical-compatibility",completeness:p,completenessReasons:b,valid:p==="complete"&&a.length===0,ir:A}}function it(e){let t=new Map(e.files.map(n=>[Q(n.path),n]));for(let n of e.changes){let r=Q(n.path);"delete"in n&&n.delete?t.delete(r):"content"in n&&t.set(r,{path:r,content:n.content})}return Te({contract:e.contract,files:[...t.values()],compilerOptions:e.compilerOptions})}function ir(e){let t=`${e.evidence.file}:${e.evidence.line}`;if(!e.edge)return`${e.ruleId} at ${t}: ${e.message}`;let n=e.edge.to??e.edge.specifier;return`${e.ruleId} at ${t}: ${e.edge.from} imports ${n}. ${e.message}`}function _e(e){switch(e.ruleId){case"LAYER_IMPORT_VIOLATION":return e.typeOnly||e.targetTypeOnlyExports||e.namedBindingsTypeOnly?"Move the referenced type to a mutually allowed layer, use `import type`, then preflight again.":e.peerIsolation?"Extract the shared dependency to a shared layer, then preflight again.":`Define a port in ${e.fromLayer??"the source layer"}, inject the ${e.toLayer??"outer-layer"} implementation, then preflight again.`;case"FORBIDDEN_GLOBAL":return`Inject ${e.target??"the capability"} through a port, then preflight again.`;case"CAPABILITY_VIOLATION":return`Define a ${String(e.capability??"capability")} port in ${e.fromLayer??"the walled layer"}, bind the implementation outside it, then preflight again.`;case"CIRCULAR_DEPENDENCY":return"Extract the shared dependency into a third module, then preflight again.";case"RAW_EVENT_PUBLISH":return"Publish through a registered intent creator, then run Ark again.";case"PUBLISH_MISSING_SOURCE":return"Add metadata.source to the publish call, then run Ark again.";case"ARKRULE_STRUCTURE":case"ARKRULE_INVARIANT":case"INVARIANT_UNCOVERED":return`Fix the structure or invariant for ${typeof e.arkruleId=="string"&&e.arkruleId.length>0?e.arkruleId:"the ArkRule"} (declared in ${typeof e.arkruleSource=="string"&&e.arkruleSource.length>0?e.arkruleSource:"arkrules/<Layer>.json"}), then preflight again. Do not demote the rule without a hash-bound policy acknowledgement.`;default:return typeof e.ruleId=="string"&&e.ruleId.startsWith("ARKRULE_")?`Fix the ArkRule ${typeof e.arkruleId=="string"?e.arkruleId:e.ruleId}, then preflight again.`:`Resolve ${typeof e.ruleId=="string"&&e.ruleId.length>0?e.ruleId:"ARK_UNKNOWN"} without weakening ark.config.json, then run Ark again.`}}function Jt(e){return E(S(e.map(({path:t,contentHash:n})=>({path:t,contentHash:n}))))}function or(e){let t=Te(e),n=new Map(t.ir.files.map(c=>[c.path,c])),r=[],s=new Set,a=[];for(let c of e.changes){let f=c.path.replace(/\\/g,"/"),y=Q(c.path);if(!y||y===".."||y.startsWith("../")||f.startsWith("/")||/^[A-Za-z]:\//.test(f)||f.includes("\0")){a.push({ruleId:"INVALID_CHANGE_PATH",file:"<change-set>",line:1,message:"Every change requires a safe, non-empty project-relative path."});continue}if(s.has(y)){a.push({ruleId:"DUPLICATE_CHANGE_PATH",file:y,line:1,message:`The atomic change set contains more than one operation for ${y}.`});continue}s.add(y),"delete"in c&&c.delete&&!n.has(y)&&a.push({ruleId:"DELETE_TARGET_MISSING",file:y,line:1,message:`Cannot delete ${y} because it is not present in the supplied base tree.`}),r.push("delete"in c&&c.delete?{path:y,delete:!0}:{path:y,content:"content"in c?c.content:""})}e.changes.length===0&&a.push({ruleId:"CHANGE_SET_EMPTY",file:"<change-set>",line:1,message:"Atomic preflight requires at least one create, update, or delete."});let i=it({...e,changes:r}),d=new Map(i.ir.files.map(c=>[c.path,c])),p=i.ir.violations.filter(c=>c.ruleId==="CAPABILITY_VIOLATION"||c.ruleId==="FORBIDDEN_GLOBAL").map(c=>{let f=d.get(c.evidence.file)?.layer;return{ruleId:c.ruleId,file:c.evidence.file,line:c.evidence.line,target:c.symbol,...c.capability?{capability:c.capability}:{},...f?{fromLayer:f}:{},edgeKind:"import",message:c.message}}),b=ge({config:e.contract.config,rules:e.contract.config.rules,files:i.ir.files.map(c=>c.path),contentViolations:p,edges:i.ir.edges.filter(c=>!!c.fromLayer).map(c=>({from:c.from,fromLayer:c.fromLayer,...c.to?{to:c.to}:{},...c.toLayer?{toLayer:c.toLayer}:{},line:c.evidence.line,kind:"import",...c.typeOnly?{typeOnly:!0}:{},...c.namedBindingsTypeOnly?{namedBindingsTypeOnly:!0}:{}}))}),A=r.map(c=>{let f=Q(c.path),y=n.get(f),R=d.get(f);return{path:f,operation:"delete"in c&&c.delete?"delete":y?"update":"create",...y?{beforeContentHash:y.contentHash}:{},...R?{candidateContentHash:R.contentHash}:{}}}).sort((c,f)=>c.path.localeCompare(f.path)),h=[...a,...b.violations].map(c=>({...c,nextAction:_e(c)})),o=h.filter(c=>c.failsStrict!==!1),l=e.changeMap?be({changeMap:e.changeMap,changes:A,baseDependencies:t.ir.edges.flatMap(c=>c.to?[{from:c.from,to:c.to}]:[]),candidateDependencies:i.ir.edges.flatMap(c=>c.to?[{from:c.from,to:c.to}]:[])}):void 0;return{schemaVersion:"1.0",mode:"lexical-compatibility",valid:t.completeness==="complete"&&i.valid&&o.length===0&&(l?.structurallyConverged??!0),readOnly:!0,policyHash:e.contract.policyHash,compilerOptionsHash:i.ir.compilerOptionsHash,baseTreeHash:Jt(t.ir.files),candidateTreeHash:Jt(i.ir.files),baseCompleteness:t.completeness,candidateCompleteness:i.completeness,baseCompletenessReasons:t.completenessReasons,candidateCompletenessReasons:i.completenessReasons,...e.changeMap?{changeMapHash:e.changeMap.hash}:{},...l?{convergence:l}:{},changes:A,violations:h,warnings:b.warnings}}function lr(e){let t=e.replace(/\\/g,"/");if(!t||t.startsWith("/")||/^[A-Za-z]:\//.test(t)||t.includes("\0"))return;let n=[];for(let s of t.split("/"))if(!(!s||s==="."))if(s===".."){if(n.length===0)return;n.pop()}else n.push(s);let r=n.join("/");return r&&r===t?r:void 0}function cr(e,t){return[["resolverIdentity",e.resolverIdentity,t.resolverIdentity],["compilerIdentity",e.compilerIdentity,t.compilerIdentity],["evidenceRequirementsHash",e.evidenceRequirementsHash,t.evidenceRequirementsHash],["projectPackageName",e.projectPackageName??"",t.projectPackageName??""]].filter(([,r,s])=>r!==s).map(([r,s,a])=>({ruleId:"FACTS_IDENTITY_MISMATCH",file:"<change-set>",line:1,field:r,before:s,candidate:a,message:`Base and candidate facts must use the same ${r}.`}))}function dr(e,t,n,r){let s=n.get(t),a=r.get(t);return{path:t,operation:"delete"in e&&e.delete?"delete":s?"update":"create",...s?{beforeContentHash:s.contentHash}:{},...a?{candidateContentHash:a.contentHash}:{}}}function pr(e){let t=W(e.baseFacts),n=W(e.candidateFacts),r=$e({contract:e.contract,facts:t}),s=$e({contract:e.contract,facts:n}),a=new Map(t.files.map(l=>[l.path,l])),i=new Map(n.files.map(l=>[l.path,l])),d=cr(t,n),p=new Map,b=[];for(let l of e.changes){let c=lr(l.path);if(!c){d.push({ruleId:"INVALID_CHANGE_PATH",file:"<change-set>",line:1,message:"Every change requires a canonical project-relative path."});continue}if(p.has(c)){d.push({ruleId:"DUPLICATE_CHANGE_PATH",file:c,line:1,message:`The atomic change set contains more than one operation for ${c}.`});continue}p.set(c,l);let f=a.get(c),y=i.get(c);if("delete"in l&&l.delete)f||d.push({ruleId:"DELETE_TARGET_MISSING",file:c,line:1,message:`Cannot delete ${c} because it is not present in the supplied base facts.`}),y&&d.push({ruleId:"CANDIDATE_DELETE_NOT_APPLIED",file:c,line:1,message:`Candidate facts still contain deleted file ${c}.`});else{let R="content"in l?l.content:"",u=E(R);y?y.contentHash!==u&&d.push({ruleId:"CANDIDATE_CONTENT_HASH_MISMATCH",file:c,line:1,expectedContentHash:u,candidateContentHash:y.contentHash,message:`Candidate facts for ${c} do not match the declared content.`}):d.push({ruleId:"CANDIDATE_CHANGE_MISSING",file:c,line:1,message:`Candidate facts do not contain changed file ${c}.`})}b.push(dr(l,c,a,i))}e.changes.length===0&&d.push({ruleId:"CHANGE_SET_EMPTY",file:"<change-set>",line:1,message:"Atomic preflight requires at least one create, update, or delete."});for(let l of new Set([...a.keys(),...i.keys()])){if(p.has(l))continue;let c=a.get(l),f=i.get(l);c&&f&&S(c)===S(f)||d.push({ruleId:"UNDECLARED_CANDIDATE_CHANGE",file:l,line:1,message:`Candidate facts change ${l}, but the atomic change set does not declare it.`})}let A=e.changeMap?be({changeMap:e.changeMap,changes:b,baseDependencies:r.ir.edges.flatMap(l=>l.to?[{from:l.from,to:l.to}]:[]),candidateDependencies:s.ir.edges.flatMap(l=>l.to?[{from:l.from,to:l.to}]:[])}):void 0,h=[...d,...s.ir.violations].map(l=>({...l,nextAction:_e(l)})),o=h.filter(l=>l.failsStrict!==!1);return{schemaVersion:"1.0",mode:"resolved-candidate-facts",valid:r.completeness==="complete"&&s.strictValid&&o.length===0&&(A?.structurallyConverged??!0),readOnly:!0,policyHash:e.contract.policyHash,resolverIdentity:n.resolverIdentity,compilerIdentity:n.compilerIdentity,compilerOptionsHash:n.compilerOptionsHash,tsconfigHash:n.tsconfigHash,baseCompilerOptionsHash:t.compilerOptionsHash,candidateCompilerOptionsHash:n.compilerOptionsHash,baseTsconfigHash:t.tsconfigHash,candidateTsconfigHash:n.tsconfigHash,evidenceRequirementsHash:n.evidenceRequirementsHash,baseFactsHash:t.factsHash,candidateFactsHash:n.factsHash,baseTreeHash:t.candidateTreeHash,candidateTreeHash:n.candidateTreeHash,baseCompleteness:r.completeness,candidateCompleteness:s.completeness,baseCompletenessReasons:r.completenessReasons,candidateCompletenessReasons:s.completenessReasons,...e.changeMap?{changeMapHash:e.changeMap.hash}:{},...A?{convergence:A}:{},changes:b.sort((l,c)=>l.path<c.path?-1:l.path>c.path?1:0),violations:h,warnings:s.ir.warnings}}var Zt=new WeakSet;function Xt(e){if(!e||typeof e!="object"||Object.isFrozen(e))return e;for(let t of Object.values(e))Xt(t);return Object.freeze(e)}function Ys(e){let t=Xt(Ie(e));return Zt.add(t),t}function Ws(e){if(!Zt.has(e.facts))throw new Error("Trusted resolved analysis requires immutable in-process canonical facts.");return Ye(e)}export{De as AMBIENT_CAPABILITY_ENTRIES,Me as CAPABILITY_IDS,Ge as DEFAULT_INTENT_PREFIXES,ut as RESOLVED_CANDIDATE_FACTS_SCHEMA,ee as RESOLVED_CANDIDATE_FACTS_SCHEMA_VERSION,Ue as SOURCE_POLICY_MESSAGES,je as ambientCoveredByForbiddenGlobals,be as analyzeArchitectureConvergence,it as analyzeChange,ar as analyzePolicyDelta,Te as analyzeProject,$e as analyzeResolvedProject,Ws as analyzeTrustedResolvedProject,ue as capabilityForAmbientName,pe as capabilityForModuleSpecifier,ze as classifyPublishFacts,Ke as collectAnalysisConfigWarnings,Hn as collectCapabilityUses,tt as collectForbiddenCapabilityUses,Ie as createResolvedCandidateFacts,Ys as createTrustedResolvedCandidateFacts,wt as detectArchitectureCycles,E as deterministicHash,se as effectiveCapabilityDeny,ge as evaluateArchitectureGraph,ir as explainViolation,et as extractSemanticDependencies,fe as forbiddenGlobalForModuleSpecifier,Ln as loadArchitectureChangeMap,at as loadContract,W as loadResolvedCandidateFacts,bt as looksLikeArkIntent,an as lowerForbiddenGlobal,Se as loweredLayerCoverage,or as preflightChange,pr as preflightResolvedChange,qe as resolveIntentLayer,ve as resolvedFactsEvidenceRequirementsHash,S as stableSerialize};
10
+ `).length,d={kind:"import",file:e.path,line:i,excerpt:s.excerpt},p=tr(e.path,a,t);n.push({from:e.path,specifier:a,to:p?.path??null,resolution:p?"resolved":"unresolved",fromLayer:e.layer,toLayer:p?.layer??null,evidence:d})}return{edges:n,capabilityUses:r}}function tr(e,t,n){let r=e.split("/");r.pop();for(let a of t.split("/"))a==="."||a===""||(a===".."?r.pop():r.push(a));let s=r.join("/");for(let a of[s,`${s}.ts`,`${s}.tsx`,`${s}.mts`,`${s}.cts`,`${s}/index.ts`,`${s}/index.tsx`]){let i=n.get(a);if(i)return i}}function Wt(e,t){let n=[];for(let r of e){if(!r.to||!r.fromLayer||!r.toLayer)continue;let s=ae(t.rules,r.fromLayer,r.toLayer,{fromPath:r.from,toPath:r.to,layers:t.layers});s&&n.push({ruleId:`layer-dependency:${s.from}->${s.to}`,message:s.message??`${s.from} must not depend on ${s.to}.`,edge:r,evidence:r.evidence})}return n}var nr={code:"LEXICAL_EVIDENCE_INCOMPLETE",message:"Lexical compatibility mode cannot prove parse status, TypeScript/package/symlink resolution, or symbol-aware source-policy and safety evidence. Use the resolved candidate facts APIs for an authoritative verdict."};function rr(e,t){return e.arkRules!==void 0&&Object.keys(e.arkRules).length>0||t.structure.length>0||t.invariants.length>0}function sr(e,t){return rr(e,t)?E(S(Dt({config:e,arkRules:t,warnings:[]}))):E(S(e))}function at(e,t,n){let r=typeof e=="string"?Ht(e,t):st(e,t),s=n?.arkRules??me();return{...r,arkRules:s,policyHash:sr(r.config,s)}}function ar(e){let t=at(e.baseConfig,e.baseSource??"base ark.config.json",{arkRules:e.baseArkRules}),n=at(e.candidateConfig,e.candidateSource??"candidate ark.config.json",{arkRules:e.candidateArkRules}),r=Gt(t.config,n.config,{baseArkRules:t.arkRules,candidateArkRules:n.arkRules,candidateInvariantCoverage:e.candidateInvariantCoverage}),s=r.findings.filter(d=>d.classification==="weakening"||d.classification==="judgment-required").map(d=>d.id).sort(),a=s.length>0,i=a&&qt(e.acknowledgement,{basePolicyHash:t.policyHash,candidatePolicyHash:n.policyHash,findingIds:s});return{schemaVersion:r.schemaVersion,basePolicyHash:t.policyHash,candidatePolicyHash:n.policyHash,classification:r.classification,findings:r.findings,blockingFindingIds:s,requiresAcknowledgement:a,acknowledged:i,valid:!a||i}}function Te(e){let t=e.files.map(h=>{let o=Q(h.path);return{path:o,content:h.content,contentHash:E(h.content),layer:V(o,e.contract.config.layers)??null}}).sort((h,o)=>h.path.localeCompare(o.path)),n=new Map(t.map(h=>[h.path,h])),r=[],s=[];for(let h of t){let o=Yt(h,n);r.push(...o.edges),s.push(...o.capabilityUses)}let a=Wt(r,e.contract.config),i=new Map(e.contract.config.layers.map(h=>[h.name,h])),d=new Map(e.contract.config.layers.map(h=>[h.name,new Set(se(h))]));for(let h of s){let o=n.get(h.file)?.layer;if(!o)continue;let l=i.get(o),c=fe(h.symbol,l?.forbiddenGlobals??[]);if(c){a.push({ruleId:"FORBIDDEN_GLOBAL",message:`${o} must not use module "${h.symbol}" because it is the import form of forbidden global "${c}".`,symbol:h.symbol,evidence:h.evidence});continue}d.get(o)?.has(h.capability)&&a.push({ruleId:"CAPABILITY_VIOLATION",message:`${o} denies the ${h.capability} capability; found import of "${h.symbol}".`,capability:h.capability,symbol:h.symbol,evidence:h.evidence})}let p=t.length===0?"complete":"partial",b=p==="complete"?[]:[{...nr}],A={schemaVersion:mt,policyHash:e.contract.policyHash,compilerOptionsHash:E(S(e.compilerOptions??{})),files:t,layers:e.contract.config.layers.map(h=>h.name),edges:r,capabilityUses:s,violations:a};return{mode:"lexical-compatibility",completeness:p,completenessReasons:b,valid:p==="complete"&&a.length===0,ir:A}}function it(e){let t=new Map(e.files.map(n=>[Q(n.path),n]));for(let n of e.changes){let r=Q(n.path);"delete"in n&&n.delete?t.delete(r):"content"in n&&t.set(r,{path:r,content:n.content})}return Te({contract:e.contract,files:[...t.values()],compilerOptions:e.compilerOptions})}function ir(e){let t=`${e.evidence.file}:${e.evidence.line}`;if(!e.edge)return`${e.ruleId} at ${t}: ${e.message}`;let n=e.edge.to??e.edge.specifier;return`${e.ruleId} at ${t}: ${e.edge.from} imports ${n}. ${e.message}`}function _e(e){switch(e.ruleId){case"LAYER_IMPORT_VIOLATION":return e.typeOnly||e.targetTypeOnlyExports||e.namedBindingsTypeOnly?"Move the referenced type to a mutually allowed layer, use `import type`, then preflight again.":e.peerIsolation?"Extract the shared dependency to a shared layer, test at the public interface, then preflight again.":`Define a port in ${e.fromLayer??"the source layer"}, inject the ${e.toLayer??"outer-layer"} implementation, test at the public interface, then preflight again.`;case"FORBIDDEN_GLOBAL":return`Inject ${e.target??"the capability"} through a port, test at the public interface, then preflight again.`;case"CAPABILITY_VIOLATION":return`Define a ${String(e.capability??"capability")} port in ${e.fromLayer??"the walled layer"}, bind the implementation outside it, test at the public interface, then preflight again.`;case"CIRCULAR_DEPENDENCY":return"Extract the shared dependency into a third module, test at the public interface, then preflight again.";case"RAW_EVENT_PUBLISH":return"Publish through a registered intent creator, then run Ark again.";case"PUBLISH_MISSING_SOURCE":return"Add metadata.source to the publish call, then run Ark again.";case"ARKRULE_STRUCTURE":case"ARKRULE_INVARIANT":case"INVARIANT_UNCOVERED":return`Fix the structure or invariant for ${typeof e.arkruleId=="string"&&e.arkruleId.length>0?e.arkruleId:"the ArkRule"} (declared in ${typeof e.arkruleSource=="string"&&e.arkruleSource.length>0?e.arkruleSource:"arkrules/<Layer>.json"}), then preflight again. Do not demote the rule without a hash-bound policy acknowledgement.`;default:return typeof e.ruleId=="string"&&e.ruleId.startsWith("ARKRULE_")?`Fix the ArkRule ${typeof e.arkruleId=="string"?e.arkruleId:e.ruleId}, then preflight again.`:`Resolve ${typeof e.ruleId=="string"&&e.ruleId.length>0?e.ruleId:"ARK_UNKNOWN"} without weakening ark.config.json, then run Ark again.`}}function Jt(e){return E(S(e.map(({path:t,contentHash:n})=>({path:t,contentHash:n}))))}function or(e){let t=Te(e),n=new Map(t.ir.files.map(c=>[c.path,c])),r=[],s=new Set,a=[];for(let c of e.changes){let f=c.path.replace(/\\/g,"/"),y=Q(c.path);if(!y||y===".."||y.startsWith("../")||f.startsWith("/")||/^[A-Za-z]:\//.test(f)||f.includes("\0")){a.push({ruleId:"INVALID_CHANGE_PATH",file:"<change-set>",line:1,message:"Every change requires a safe, non-empty project-relative path."});continue}if(s.has(y)){a.push({ruleId:"DUPLICATE_CHANGE_PATH",file:y,line:1,message:`The atomic change set contains more than one operation for ${y}.`});continue}s.add(y),"delete"in c&&c.delete&&!n.has(y)&&a.push({ruleId:"DELETE_TARGET_MISSING",file:y,line:1,message:`Cannot delete ${y} because it is not present in the supplied base tree.`}),r.push("delete"in c&&c.delete?{path:y,delete:!0}:{path:y,content:"content"in c?c.content:""})}e.changes.length===0&&a.push({ruleId:"CHANGE_SET_EMPTY",file:"<change-set>",line:1,message:"Atomic preflight requires at least one create, update, or delete."});let i=it({...e,changes:r}),d=new Map(i.ir.files.map(c=>[c.path,c])),p=i.ir.violations.filter(c=>c.ruleId==="CAPABILITY_VIOLATION"||c.ruleId==="FORBIDDEN_GLOBAL").map(c=>{let f=d.get(c.evidence.file)?.layer;return{ruleId:c.ruleId,file:c.evidence.file,line:c.evidence.line,target:c.symbol,...c.capability?{capability:c.capability}:{},...f?{fromLayer:f}:{},edgeKind:"import",message:c.message}}),b=ge({config:e.contract.config,rules:e.contract.config.rules,files:i.ir.files.map(c=>c.path),contentViolations:p,edges:i.ir.edges.filter(c=>!!c.fromLayer).map(c=>({from:c.from,fromLayer:c.fromLayer,...c.to?{to:c.to}:{},...c.toLayer?{toLayer:c.toLayer}:{},line:c.evidence.line,kind:"import",...c.typeOnly?{typeOnly:!0}:{},...c.namedBindingsTypeOnly?{namedBindingsTypeOnly:!0}:{}}))}),A=r.map(c=>{let f=Q(c.path),y=n.get(f),R=d.get(f);return{path:f,operation:"delete"in c&&c.delete?"delete":y?"update":"create",...y?{beforeContentHash:y.contentHash}:{},...R?{candidateContentHash:R.contentHash}:{}}}).sort((c,f)=>c.path.localeCompare(f.path)),h=[...a,...b.violations].map(c=>({...c,nextAction:_e(c)})),o=h.filter(c=>c.failsStrict!==!1),l=e.changeMap?be({changeMap:e.changeMap,changes:A,baseDependencies:t.ir.edges.flatMap(c=>c.to?[{from:c.from,to:c.to}]:[]),candidateDependencies:i.ir.edges.flatMap(c=>c.to?[{from:c.from,to:c.to}]:[])}):void 0;return{schemaVersion:"1.0",mode:"lexical-compatibility",valid:t.completeness==="complete"&&i.valid&&o.length===0&&(l?.structurallyConverged??!0),readOnly:!0,policyHash:e.contract.policyHash,compilerOptionsHash:i.ir.compilerOptionsHash,baseTreeHash:Jt(t.ir.files),candidateTreeHash:Jt(i.ir.files),baseCompleteness:t.completeness,candidateCompleteness:i.completeness,baseCompletenessReasons:t.completenessReasons,candidateCompletenessReasons:i.completenessReasons,...e.changeMap?{changeMapHash:e.changeMap.hash}:{},...l?{convergence:l}:{},changes:A,violations:h,warnings:b.warnings}}function lr(e){let t=e.replace(/\\/g,"/");if(!t||t.startsWith("/")||/^[A-Za-z]:\//.test(t)||t.includes("\0"))return;let n=[];for(let s of t.split("/"))if(!(!s||s==="."))if(s===".."){if(n.length===0)return;n.pop()}else n.push(s);let r=n.join("/");return r&&r===t?r:void 0}function cr(e,t){return[["resolverIdentity",e.resolverIdentity,t.resolverIdentity],["compilerIdentity",e.compilerIdentity,t.compilerIdentity],["evidenceRequirementsHash",e.evidenceRequirementsHash,t.evidenceRequirementsHash],["projectPackageName",e.projectPackageName??"",t.projectPackageName??""]].filter(([,r,s])=>r!==s).map(([r,s,a])=>({ruleId:"FACTS_IDENTITY_MISMATCH",file:"<change-set>",line:1,field:r,before:s,candidate:a,message:`Base and candidate facts must use the same ${r}.`}))}function dr(e,t,n,r){let s=n.get(t),a=r.get(t);return{path:t,operation:"delete"in e&&e.delete?"delete":s?"update":"create",...s?{beforeContentHash:s.contentHash}:{},...a?{candidateContentHash:a.contentHash}:{}}}function pr(e){let t=W(e.baseFacts),n=W(e.candidateFacts),r=$e({contract:e.contract,facts:t}),s=$e({contract:e.contract,facts:n}),a=new Map(t.files.map(l=>[l.path,l])),i=new Map(n.files.map(l=>[l.path,l])),d=cr(t,n),p=new Map,b=[];for(let l of e.changes){let c=lr(l.path);if(!c){d.push({ruleId:"INVALID_CHANGE_PATH",file:"<change-set>",line:1,message:"Every change requires a canonical project-relative path."});continue}if(p.has(c)){d.push({ruleId:"DUPLICATE_CHANGE_PATH",file:c,line:1,message:`The atomic change set contains more than one operation for ${c}.`});continue}p.set(c,l);let f=a.get(c),y=i.get(c);if("delete"in l&&l.delete)f||d.push({ruleId:"DELETE_TARGET_MISSING",file:c,line:1,message:`Cannot delete ${c} because it is not present in the supplied base facts.`}),y&&d.push({ruleId:"CANDIDATE_DELETE_NOT_APPLIED",file:c,line:1,message:`Candidate facts still contain deleted file ${c}.`});else{let R="content"in l?l.content:"",u=E(R);y?y.contentHash!==u&&d.push({ruleId:"CANDIDATE_CONTENT_HASH_MISMATCH",file:c,line:1,expectedContentHash:u,candidateContentHash:y.contentHash,message:`Candidate facts for ${c} do not match the declared content.`}):d.push({ruleId:"CANDIDATE_CHANGE_MISSING",file:c,line:1,message:`Candidate facts do not contain changed file ${c}.`})}b.push(dr(l,c,a,i))}e.changes.length===0&&d.push({ruleId:"CHANGE_SET_EMPTY",file:"<change-set>",line:1,message:"Atomic preflight requires at least one create, update, or delete."});for(let l of new Set([...a.keys(),...i.keys()])){if(p.has(l))continue;let c=a.get(l),f=i.get(l);c&&f&&S(c)===S(f)||d.push({ruleId:"UNDECLARED_CANDIDATE_CHANGE",file:l,line:1,message:`Candidate facts change ${l}, but the atomic change set does not declare it.`})}let A=e.changeMap?be({changeMap:e.changeMap,changes:b,baseDependencies:r.ir.edges.flatMap(l=>l.to?[{from:l.from,to:l.to}]:[]),candidateDependencies:s.ir.edges.flatMap(l=>l.to?[{from:l.from,to:l.to}]:[])}):void 0,h=[...d,...s.ir.violations].map(l=>({...l,nextAction:_e(l)})),o=h.filter(l=>l.failsStrict!==!1);return{schemaVersion:"1.0",mode:"resolved-candidate-facts",valid:r.completeness==="complete"&&s.strictValid&&o.length===0&&(A?.structurallyConverged??!0),readOnly:!0,policyHash:e.contract.policyHash,resolverIdentity:n.resolverIdentity,compilerIdentity:n.compilerIdentity,compilerOptionsHash:n.compilerOptionsHash,tsconfigHash:n.tsconfigHash,baseCompilerOptionsHash:t.compilerOptionsHash,candidateCompilerOptionsHash:n.compilerOptionsHash,baseTsconfigHash:t.tsconfigHash,candidateTsconfigHash:n.tsconfigHash,evidenceRequirementsHash:n.evidenceRequirementsHash,baseFactsHash:t.factsHash,candidateFactsHash:n.factsHash,baseTreeHash:t.candidateTreeHash,candidateTreeHash:n.candidateTreeHash,baseCompleteness:r.completeness,candidateCompleteness:s.completeness,baseCompletenessReasons:r.completenessReasons,candidateCompletenessReasons:s.completenessReasons,...e.changeMap?{changeMapHash:e.changeMap.hash}:{},...A?{convergence:A}:{},changes:b.sort((l,c)=>l.path<c.path?-1:l.path>c.path?1:0),violations:h,warnings:s.ir.warnings}}var Zt=new WeakSet;function Xt(e){if(!e||typeof e!="object"||Object.isFrozen(e))return e;for(let t of Object.values(e))Xt(t);return Object.freeze(e)}function Ys(e){let t=Xt(Ie(e));return Zt.add(t),t}function Ws(e){if(!Zt.has(e.facts))throw new Error("Trusted resolved analysis requires immutable in-process canonical facts.");return Ye(e)}export{De as AMBIENT_CAPABILITY_ENTRIES,Me as CAPABILITY_IDS,Ge as DEFAULT_INTENT_PREFIXES,ut as RESOLVED_CANDIDATE_FACTS_SCHEMA,ee as RESOLVED_CANDIDATE_FACTS_SCHEMA_VERSION,Ue as SOURCE_POLICY_MESSAGES,je as ambientCoveredByForbiddenGlobals,be as analyzeArchitectureConvergence,it as analyzeChange,ar as analyzePolicyDelta,Te as analyzeProject,$e as analyzeResolvedProject,Ws as analyzeTrustedResolvedProject,ue as capabilityForAmbientName,pe as capabilityForModuleSpecifier,ze as classifyPublishFacts,Ke as collectAnalysisConfigWarnings,Hn as collectCapabilityUses,tt as collectForbiddenCapabilityUses,Ie as createResolvedCandidateFacts,Ys as createTrustedResolvedCandidateFacts,wt as detectArchitectureCycles,E as deterministicHash,se as effectiveCapabilityDeny,ge as evaluateArchitectureGraph,ir as explainViolation,et as extractSemanticDependencies,fe as forbiddenGlobalForModuleSpecifier,Ln as loadArchitectureChangeMap,at as loadContract,W as loadResolvedCandidateFacts,bt as looksLikeArkIntent,an as lowerForbiddenGlobal,Se as loweredLayerCoverage,or as preflightChange,pr as preflightResolvedChange,qe as resolveIntentLayer,ve as resolvedFactsEvidenceRequirementsHash,S as stableSerialize};
@@ -414,6 +414,11 @@ ${projectionBlock}
414
414
  - **Design-weak / residual shape lenses** (compass residual while edges may look green) → map first, then guided apply with user OK — never “you’re done” on green edges alone.
415
415
  - Empty plan A + residual lenses / design-weak → **not finished**.
416
416
 
417
+ **Two-axis done (never collapse):**
418
+ 1. **Architecture residual** — status / doctor / compass (scan). Prefer deep modules; name seams; test at the public interface.
419
+ 2. **Feature / ticket residual** — does the change match the requested behavior? (process outside the package).
420
+ **Enforce green ≠ feature done.** No package LLM verdict for either axis.
421
+
417
422
  The selected host is \`${selectedHost}\`. Host registration and CI are installed with this file.
418
423
  This compact router is enough for normal feature work.
419
424
 
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Deep-module coach advisory (Tooling) — hot paths + deepening candidates.
3
+ *
4
+ * Advisory only (`notAScore`). Never feeds valid / strict-merge / goal.met /
5
+ * completeness green. Hot paths use a bounded git log heuristic; incomplete or
6
+ * missing history → status `unavailable`, empty paths (never invent).
7
+ * Deepening candidates come only from existing smells / cohesion / compass /
8
+ * pilot evidence via Domain pure projection.
9
+ */
10
+ import { spawnSync } from 'node:child_process';
11
+ import {
12
+ buildDeepeningCandidates,
13
+ ARK_DEEPENING_COACH_SCHEMA_VERSION,
14
+ DEEPENING_CANDIDATE_CAP,
15
+ } from './deepening-coach.mjs';
16
+
17
+ export { buildDeepeningCandidates, ARK_DEEPENING_COACH_SCHEMA_VERSION, DEEPENING_CANDIDATE_CAP };
18
+
19
+ /** Recent-window commit cap for hot-path heuristic (budget). */
20
+ export const HOT_PATH_COMMIT_LIMIT = 200;
21
+ /** Max listed hot paths. */
22
+ export const HOT_PATH_LIST_CAP = 8;
23
+ /** Minimum change hits before a path is “elevated”. */
24
+ export const HOT_PATH_MIN_HITS = 3;
25
+
26
+ /**
27
+ * Best-effort recent-churn paths from git history.
28
+ * @param {string} root
29
+ * @param {{ runGit?: Function, commitLimit?: number, minHits?: number, listCap?: number }} [opts]
30
+ */
31
+ export function computeHotPathAdvisory(root, opts = {}) {
32
+ const commitLimit = Number(opts.commitLimit) > 0 ? Number(opts.commitLimit) : HOT_PATH_COMMIT_LIMIT;
33
+ const minHits = Number(opts.minHits) > 0 ? Number(opts.minHits) : HOT_PATH_MIN_HITS;
34
+ const listCap = Number(opts.listCap) > 0 ? Number(opts.listCap) : HOT_PATH_LIST_CAP;
35
+ const runGit =
36
+ typeof opts.runGit === 'function'
37
+ ? opts.runGit
38
+ : (args) =>
39
+ spawnSync('git', ['-C', root, ...args], {
40
+ encoding: 'utf8',
41
+ maxBuffer: 8 * 1024 * 1024,
42
+ stdio: ['ignore', 'pipe', 'pipe'],
43
+ });
44
+
45
+ const emptyUnavailable = (reason) => ({
46
+ available: false,
47
+ status: 'unavailable',
48
+ reason,
49
+ paths: [],
50
+ notAScore: true,
51
+ });
52
+
53
+ try {
54
+ const head = runGit(['rev-parse', '--verify', 'HEAD']);
55
+ if (!head || head.status !== 0) {
56
+ return emptyUnavailable('git history incomplete or missing (no HEAD)');
57
+ }
58
+
59
+ const log = runGit([
60
+ 'log',
61
+ '-n',
62
+ String(commitLimit),
63
+ '--name-only',
64
+ '--pretty=format:',
65
+ '--diff-filter=AMR',
66
+ ]);
67
+ if (!log || log.status !== 0) {
68
+ return emptyUnavailable('git log unavailable for hot-path heuristic');
69
+ }
70
+
71
+ const counts = new Map();
72
+ const text = typeof log.stdout === 'string' ? log.stdout : '';
73
+ for (const line of text.split('\n')) {
74
+ const raw = line.trim().replace(/\\/g, '/');
75
+ if (!raw || raw.startsWith('.git/')) continue;
76
+ // Prefer product source; still allow other paths if they dominate.
77
+ counts.set(raw, (counts.get(raw) || 0) + 1);
78
+ }
79
+
80
+ if (counts.size === 0) {
81
+ return {
82
+ available: true,
83
+ status: 'ok',
84
+ reason: null,
85
+ paths: [],
86
+ notAScore: true,
87
+ window: { commitLimit, minHits },
88
+ };
89
+ }
90
+
91
+ const ranked = [...counts.entries()]
92
+ .filter(([, n]) => n >= minHits)
93
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
94
+ .slice(0, listCap)
95
+ .map(([path, changeCount]) => ({ path, changeCount }));
96
+
97
+ return {
98
+ available: true,
99
+ status: 'ok',
100
+ reason: null,
101
+ paths: ranked,
102
+ notAScore: true,
103
+ window: { commitLimit, minHits },
104
+ };
105
+ } catch {
106
+ return emptyUnavailable('git history incomplete or missing');
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Full deep-module coach advisory for doctor / report.
112
+ * @param {string} root
113
+ * @param {{
114
+ * designSmells?: object[],
115
+ * physicalCohesion?: object | null,
116
+ * improvementCompass?: object | null,
117
+ * pilotLoop?: object | null,
118
+ * runGit?: Function,
119
+ * }} [input]
120
+ */
121
+ export function buildDeepModuleCoachAdvisory(root, input = {}) {
122
+ const deepening = buildDeepeningCandidates({
123
+ designSmells: input.designSmells,
124
+ physicalCohesion: input.physicalCohesion,
125
+ improvementCompass: input.improvementCompass,
126
+ pilotLoop: input.pilotLoop,
127
+ });
128
+ const hotPaths = computeHotPathAdvisory(root, {
129
+ runGit: input.runGit,
130
+ });
131
+
132
+ return {
133
+ schemaVersion: ARK_DEEPENING_COACH_SCHEMA_VERSION,
134
+ notAScore: true,
135
+ hotPaths,
136
+ deepeningCandidates: deepening.candidates,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Human doctor section (never a score bar). Always one short block when coach is present.
142
+ * @param {ReturnType<typeof buildDeepModuleCoachAdvisory>} coach
143
+ * @param {{ line: Function, warn: string, ok: string, color: { bold: Function, dim: Function } }} io
144
+ */
145
+ export function printDeepModuleCoachSection(coach, io) {
146
+ if (!coach || coach.notAScore !== true) return;
147
+ const { line, warn, ok, color } = io;
148
+ const hot = coach.hotPaths || { status: 'unavailable', paths: [], reason: 'missing' };
149
+ const candidates = Array.isArray(coach.deepeningCandidates) ? coach.deepeningCandidates : [];
150
+ const paths = Array.isArray(hot.paths) ? hot.paths : [];
151
+
152
+ console.log('');
153
+ console.log(color.bold('Deep-module coach (advisory — not a score)'));
154
+
155
+ if (hot.status === 'unavailable') {
156
+ line(ok, color.dim(`Hot paths: unavailable — ${hot.reason || 'no git history'}; never invented.`));
157
+ } else if (paths.length > 0) {
158
+ line(warn, `Hot paths (recent churn heuristic, top ${paths.length}):`);
159
+ for (const row of paths.slice(0, HOT_PATH_LIST_CAP)) {
160
+ line(' ', color.dim(`${row.path} · ${row.changeCount} recent change(s)`));
161
+ }
162
+ } else {
163
+ line(ok, color.dim('Hot paths: none above churn threshold (advisory only).'));
164
+ }
165
+
166
+ if (candidates.length === 0) {
167
+ line(ok, color.dim('Deepening candidates: none from existing evidence.'));
168
+ } else {
169
+ line(warn, `Deepening candidates (${candidates.length}, from existing residual only):`);
170
+ for (const c of candidates.slice(0, DEEPENING_CANDIDATE_CAP)) {
171
+ line(' ', `${c.target} — ${c.friction}`);
172
+ if (c.intent) line(' ', color.dim(`intent: ${c.intent}`));
173
+ if (c.benefit) line(' ', color.dim(`benefit: ${c.benefit}`));
174
+ }
175
+ }
176
+ line(ok, color.dim('Never changes gate verdicts. Prefer deep modules; test at the public interface.'));
177
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * GENERATED FILE — do not edit by hand.
3
+ *
4
+ * Canonical algorithm: src/domain/deepeningCoach.ts
5
+ * Regenerate: node scripts/generate-cli-pure.mjs
6
+ * Drift check: node scripts/generate-cli-pure.mjs --check
7
+ *
8
+ * Pure CLI helper (bin/lib/deepening-coach.mjs). Zero Node I/O.
9
+ */
10
+
11
+ export const ARK_DEEPENING_COACH_SCHEMA_VERSION = '1.0';
12
+ /** Cap on listed deepening candidates (agent-legible; not a ranking score). */
13
+ export const DEEPENING_CANDIDATE_CAP = 5;
14
+ function asString(value) {
15
+ return typeof value === 'string' ? value.trim() : '';
16
+ }
17
+ function firstEvidencePath(evidence) {
18
+ if (!Array.isArray(evidence))
19
+ return '';
20
+ for (const row of evidence) {
21
+ if (typeof row === 'string' && row.trim())
22
+ return row.trim().replace(/\\/g, '/');
23
+ if (row && typeof row === 'object') {
24
+ const path = asString(row.path)
25
+ || asString(row.file);
26
+ if (path)
27
+ return path.replace(/\\/g, '/');
28
+ }
29
+ }
30
+ return '';
31
+ }
32
+ function evidenceList(source, refs, detail) {
33
+ const out = [];
34
+ for (const ref of refs) {
35
+ const r = asString(ref);
36
+ if (!r)
37
+ continue;
38
+ out.push(detail ? { source, ref: r, detail } : { source, ref: r });
39
+ }
40
+ return out;
41
+ }
42
+ /**
43
+ * Project deepening candidates from existing doctor-side evidence only.
44
+ * Empty input / no residual → empty `candidates` (honesty: no fake list).
45
+ */
46
+ export function buildDeepeningCandidates(input = {}) {
47
+ const candidates = [];
48
+ const seen = new Set();
49
+ const push = (candidate) => {
50
+ if (candidates.length >= DEEPENING_CANDIDATE_CAP)
51
+ return;
52
+ const key = `${candidate.target}|${candidate.friction}`.toLowerCase();
53
+ if (seen.has(key))
54
+ return;
55
+ if (!asString(candidate.target) || !asString(candidate.friction))
56
+ return;
57
+ seen.add(key);
58
+ candidates.push(candidate);
59
+ };
60
+ const pilot = input.pilotLoop?.active === true && input.pilotLoop.nextPilot
61
+ ? input.pilotLoop.nextPilot
62
+ : null;
63
+ if (pilot) {
64
+ // Require a real pilot identity — empty shells are not evidence.
65
+ const target = asString(pilot.pilotTarget) || asString(pilot.pilot) || asString(pilot.smellId);
66
+ if (target) {
67
+ push({
68
+ target,
69
+ friction: asString(pilot.smellId)
70
+ ? `Shape pilot residual (${pilot.smellId}) — one extraction at a time`
71
+ : 'Shape pilot residual — one extraction at a time',
72
+ intent: asString(pilot.move) ||
73
+ 'Deepen the public seam; hide implementation behind a small interface',
74
+ benefit: asString(pilot.successSignal) ||
75
+ 'Locality: change and tests concentrate at the public interface',
76
+ evidence: evidenceList('pilotLoop', [target], 'nextPilot'),
77
+ });
78
+ }
79
+ }
80
+ const smells = Array.isArray(input.designSmells) ? input.designSmells : [];
81
+ for (const smell of smells) {
82
+ if (!smell || typeof smell !== 'object')
83
+ continue;
84
+ const id = asString(smell.id);
85
+ const path = firstEvidencePath(smell.evidence);
86
+ // Require non-empty smell id or evidence path — whitespace shells are not evidence.
87
+ if (!id && !path)
88
+ continue;
89
+ const target = path || id;
90
+ const friction = asString(smell.outcome) || asString(smell.message) || `Design residual (${id || target})`;
91
+ push({
92
+ target,
93
+ friction,
94
+ intent: asString(smell.fix) ||
95
+ 'Prefer a deep module at a named seam; apply the deletion test before pass-through extracts',
96
+ benefit: 'Leverage: callers learn a smaller interface; locality of change improves',
97
+ evidence: evidenceList('designSmells', path ? [path, id].filter(Boolean) : [id], id || undefined),
98
+ });
99
+ }
100
+ const pc = input.physicalCohesion;
101
+ const reshape = pc?.reshapePilot?.nextPilot;
102
+ if (reshape) {
103
+ const target = asString(reshape.pilotTarget) || asString(reshape.pilot);
104
+ if (target) {
105
+ push({
106
+ target,
107
+ friction: 'Physical cohesion residual — concept concentration across anchors',
108
+ intent: asString(reshape.move) ||
109
+ 'One reshape pilot toward locality; never mechanical-safe multi-file batch',
110
+ benefit: asString(reshape.successSignal) ||
111
+ 'Related behavior co-located; public seams stay small',
112
+ evidence: evidenceList('physicalCohesion', [target], 'reshapePilot'),
113
+ });
114
+ }
115
+ }
116
+ const findings = Array.isArray(pc?.findings) ? pc.findings : [];
117
+ for (const finding of findings) {
118
+ if (!finding || typeof finding !== 'object')
119
+ continue;
120
+ const concept = asString(finding.concept);
121
+ const anchors = Array.isArray(finding.anchors)
122
+ ? finding.anchors.map((a) => asString(a)).filter(Boolean)
123
+ : [];
124
+ const target = anchors[0] || concept;
125
+ if (!target)
126
+ continue;
127
+ push({
128
+ target,
129
+ friction: asString(finding.message) ||
130
+ (concept
131
+ ? `Concept "${concept}" concentrated across physical anchors`
132
+ : 'Physical cohesion residual'),
133
+ intent: 'Deepen by colocating behavior behind one public interface per concern',
134
+ benefit: 'Locality of change; fewer cross-anchor edits for one concept',
135
+ evidence: evidenceList('physicalCohesion', anchors.length > 0 ? anchors.slice(0, 3) : [target], concept || undefined),
136
+ });
137
+ }
138
+ const compass = input.improvementCompass;
139
+ if (compass && compass.notAScore === true) {
140
+ const residualIds = Array.isArray(compass.topResidual)
141
+ ? compass.topResidual.map((id) => asString(id)).filter(Boolean)
142
+ : [];
143
+ const lensById = new Map();
144
+ if (Array.isArray(compass.lenses)) {
145
+ for (const lens of compass.lenses) {
146
+ if (lens && typeof lens === 'object' && asString(lens.id)) {
147
+ lensById.set(asString(lens.id), { summary: asString(lens.summary) || undefined });
148
+ }
149
+ }
150
+ }
151
+ for (const id of residualIds) {
152
+ // Each residual lens id is independent evidence. Cap + target|friction dedupe may
153
+ // still list both a smell card and lens:<id> — no cross-source suppression.
154
+ const summary = lensById.get(id)?.summary;
155
+ push({
156
+ target: `lens:${id}`,
157
+ friction: summary
158
+ ? `Residual lens ${id}: ${summary}`
159
+ : `Residual architecture lens ${id} (not a score)`,
160
+ intent: 'Process judgment: deepen modules / name seams that clear this lens residual',
161
+ benefit: 'Clear residual without inventing a depth score or gate fail',
162
+ evidence: evidenceList('improvementCompass', [id], 'topResidual'),
163
+ });
164
+ }
165
+ }
166
+ return {
167
+ schemaVersion: ARK_DEEPENING_COACH_SCHEMA_VERSION,
168
+ notAScore: true,
169
+ candidates,
170
+ };
171
+ }
172
+ /**
173
+ * True when the pure projection would list zero candidates (honesty helper for tests).
174
+ */
175
+ export function hasNoDeepeningEvidence(input) {
176
+ return buildDeepeningCandidates(input).candidates.length === 0;
177
+ }
@@ -66,6 +66,10 @@ import {
66
66
  buildDoctorImprovementCompass,
67
67
  printImprovementCompassSection,
68
68
  } from './improvement-compass-doctor.mjs';
69
+ import {
70
+ buildDeepModuleCoachAdvisory,
71
+ printDeepModuleCoachSection,
72
+ } from './deep-module-coach.mjs';
69
73
 
70
74
  const color = {
71
75
  green: (s) => `\x1b[32m${s}\x1b[0m`,
@@ -660,6 +664,13 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
660
664
  goldenPatternPresent: goldenPattern.present === true,
661
665
  arkRulesLoaded: rulesUnderContract?.active === true,
662
666
  });
667
+ // Deep-module coach: hot paths + deepening candidates — advisory only (notAScore).
668
+ const deepModuleCoach = buildDeepModuleCoachAdvisory(root, {
669
+ designSmells,
670
+ physicalCohesion: doctorAdvisories.physicalCohesion,
671
+ improvementCompass,
672
+ pilotLoop,
673
+ });
663
674
 
664
675
  if (asJson) {
665
676
  (options.writeJson ?? console.log)(
@@ -677,6 +688,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
677
688
  designSmells,
678
689
  // Improvement compass (lenses; notAScore; never a gate input).
679
690
  improvementCompass,
691
+ // Deep-module coach (hot paths + deepening; notAScore; never a gate input).
692
+ deepModuleCoach,
680
693
  ...(options.designDelta ? { designDelta: options.designDelta } : {}),
681
694
  // Q01: primary next action when Shape residual dominates (null if not design-weak).
682
695
  postGreenPath,
@@ -870,6 +883,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
870
883
  }
871
884
 
872
885
  printImprovementCompassSection(improvementCompass, { line, warn, ok, color });
886
+ printDeepModuleCoachSection(deepModuleCoach, { line, warn, ok, color });
873
887
 
874
888
  console.log('');
875
889
  console.log(color.bold('Design fitness'));