arkgate 4.2.1 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -3
- package/README.md +24 -8
- package/bin/ark-check-runtime.mjs +16 -1
- package/bin/ark-mcp-runtime.mjs +64 -0
- package/bin/ark.mjs +55 -1
- package/bin/lib/adapter-contract.mjs +88 -5
- package/bin/lib/agent-projection-command.mjs +396 -0
- package/bin/lib/agent-projection.mjs +319 -0
- package/bin/lib/agent-skills-package.mjs +266 -0
- package/bin/lib/baseline-key.mjs +32 -0
- package/bin/lib/ci-and-commands.mjs +55 -5
- package/bin/lib/diagnostic-catalog.mjs +155 -0
- package/bin/lib/doctor-plan.mjs +25 -0
- package/bin/lib/html-report-advisories.mjs +33 -0
- package/bin/lib/html-report-depth.mjs +24 -0
- package/bin/lib/improvement-compass-doctor.mjs +106 -0
- package/bin/lib/improvement-compass.mjs +630 -0
- package/bin/lib/status-command.mjs +369 -0
- package/bin/lib/status-manifest.mjs +431 -0
- package/dist/eslint/index.cjs +3 -3
- package/dist/eslint/index.js +3 -3
- package/dist/index.cjs +46 -11
- package/dist/index.d.ts +886 -6
- package/dist/index.js +46 -11
- package/docs/README.md +9 -8
- package/docs/agent-guide.md +128 -14
- package/docs/configuration.md +7 -0
- package/docs/develop.md +12 -1
- package/docs/diagnostics.md +606 -0
- package/docs/package-surface.md +44 -31
- package/docs/product-voice.md +71 -0
- package/docs/use.md +60 -1
- package/package.json +7 -1
- package/schemas/ark.analysis-result.schema.json +14 -1
- package/schemas/ark.status-manifest.schema.json +270 -0
- package/server.json +2 -2
- package/templates/agent-skills/README.md +59 -0
- package/templates/agent-skills/ark-adopt/SKILL.md +191 -0
- package/templates/agent-skills/ark-architect/SKILL.md +195 -0
- package/templates/agent-skills/ark-autopilot/SKILL.md +262 -0
- package/templates/agent-skills/ark-contract/SKILL.md +156 -0
- package/templates/agent-skills/ark-coverage/SKILL.md +187 -0
- package/templates/agent-skills/ark-explain/SKILL.md +230 -0
- package/templates/agent-skills/ark-explore/SKILL.md +397 -0
- package/templates/agent-skills/ark-fix/SKILL.md +205 -0
- package/templates/agent-skills/ark-loop/SKILL.md +200 -0
- package/templates/agent-skills/ark-place/SKILL.md +182 -0
- package/templates/agent-skills/ark-runtime/SKILL.md +127 -0
- package/templates/agent-skills/ark-think/SKILL.md +153 -0
- package/templates/agent-skills/ark-upgrade/SKILL.md +238 -0
- package/templates/skills/ark-adopt.md +20 -0
- package/templates/skills/ark-architect.md +21 -1
- package/templates/skills/ark-autopilot.md +25 -5
- package/templates/skills/ark-contract.md +20 -0
- package/templates/skills/ark-coverage.md +20 -0
- package/templates/skills/ark-explain.md +20 -0
- package/templates/skills/ark-explore.md +23 -3
- package/templates/skills/ark-fix.md +22 -2
- package/templates/skills/ark-loop.md +22 -2
- package/templates/skills/ark-place.md +20 -0
- package/templates/skills/ark-runtime.md +7 -0
- package/templates/skills/ark-think.md +20 -0
- package/templates/skills/ark-upgrade.md +20 -0
|
@@ -11,10 +11,16 @@ import {
|
|
|
11
11
|
DEFAULT_INTENT_PREFIXES,
|
|
12
12
|
DEFAULT_LAYER_DIRECTORIES,
|
|
13
13
|
} from '../ark-shared.mjs';
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_AGENT_PROJECTION_RULE_IDS,
|
|
16
|
+
buildAgentProjectionBlock,
|
|
17
|
+
} from './agent-projection.mjs';
|
|
18
|
+
import { getDiagnosticCatalogEntry } from './diagnostic-catalog.mjs';
|
|
14
19
|
import { falseGreenAdoptionGap } from './field-install.mjs';
|
|
15
20
|
import { renderHostSupportMatrixMarkdown } from './host-support-matrix.mjs';
|
|
16
21
|
import { PREFERRED_MCP_BIN } from './hook-templates.mjs';
|
|
17
22
|
import { hasCheckArchitectureScript, readPackageJson } from './gate-files.mjs';
|
|
23
|
+
import { arkPackageVersion } from './skill-install.mjs';
|
|
18
24
|
|
|
19
25
|
// Field-install helpers re-exported for callers that import from this module.
|
|
20
26
|
export {
|
|
@@ -250,6 +256,37 @@ export function loadConfigLayersForAgents(root) {
|
|
|
250
256
|
}
|
|
251
257
|
}
|
|
252
258
|
|
|
259
|
+
/**
|
|
260
|
+
* ACS04 — version-matched projection block embedded in install AGENTS templates.
|
|
261
|
+
* Labeled non-authoritative; never a gate input.
|
|
262
|
+
* @param {string} root
|
|
263
|
+
* @param {{ host?: string|null, profile?: 'compact'|'full' }} [opts]
|
|
264
|
+
*/
|
|
265
|
+
export function agentProjectionBlockForRoot(root, opts = {}) {
|
|
266
|
+
const liveLayers = loadConfigLayersForAgents(root);
|
|
267
|
+
const layerSummaries = Array.isArray(liveLayers)
|
|
268
|
+
? liveLayers.map((layer) => ({
|
|
269
|
+
name: layer.name ?? layer.layer ?? 'Unknown',
|
|
270
|
+
patterns: layer.patterns ?? [],
|
|
271
|
+
intentPrefixes: layer.intentPrefixes ?? layer.prefixes ?? [],
|
|
272
|
+
}))
|
|
273
|
+
: [];
|
|
274
|
+
const catalogShortList = DEFAULT_AGENT_PROJECTION_RULE_IDS.map((ruleId) => {
|
|
275
|
+
const entry = getDiagnosticCatalogEntry(ruleId);
|
|
276
|
+
return { ruleId, title: entry?.title ?? ruleId };
|
|
277
|
+
});
|
|
278
|
+
const version = arkPackageVersion() || 'unknown';
|
|
279
|
+
return buildAgentProjectionBlock({
|
|
280
|
+
arkgateVersion: version,
|
|
281
|
+
checkCommand: arkCheckCommand(root),
|
|
282
|
+
layers: layerSummaries,
|
|
283
|
+
catalogShortList,
|
|
284
|
+
host: opts.host ?? null,
|
|
285
|
+
profile: opts.profile === 'compact' ? 'compact' : 'full',
|
|
286
|
+
diagnosticsDocsPath: 'docs/diagnostics.md',
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
253
290
|
export function agentInstructions(root) {
|
|
254
291
|
const checkCmd = arkCheckCommand(root);
|
|
255
292
|
const startCmd = arkCommand(root, 'ark', 'start');
|
|
@@ -259,6 +296,7 @@ export function agentInstructions(root) {
|
|
|
259
296
|
.join('\n');
|
|
260
297
|
const liveLayers = loadConfigLayersForAgents(root);
|
|
261
298
|
const placementTable = layerPlacementTable(liveLayers);
|
|
299
|
+
const projectionBlock = agentProjectionBlockForRoot(root, { profile: 'full' });
|
|
262
300
|
const placementBody = liveLayers
|
|
263
301
|
? `\`ark.config.json\` is authoritative for this project. Place new code in these **${liveLayers.length}** configured layer(s) — do not invent an ungoverned location or assume the stock 11-layer layout:
|
|
264
302
|
|
|
@@ -273,6 +311,7 @@ an ungoverned location:
|
|
|
273
311
|
${placementTable}`;
|
|
274
312
|
return `# Ark Enforcement
|
|
275
313
|
|
|
314
|
+
${projectionBlock}
|
|
276
315
|
## Default agent flow (if unsure, do only this)
|
|
277
316
|
|
|
278
317
|
1. Status anytime: \`${doctorCmd}\` — **control plane** (one status light, one next action; not a mode picker).
|
|
@@ -350,19 +389,30 @@ export function compactAgentInstructions(root, host = null) {
|
|
|
350
389
|
'ark-check',
|
|
351
390
|
`--install-agent-gates --skills-only --tools ${selectedHost === 'none' ? '<host>' : selectedHost}`
|
|
352
391
|
);
|
|
392
|
+
const projectionBlock = agentProjectionBlockForRoot(root, {
|
|
393
|
+
host: selectedHost === 'none' ? null : selectedHost,
|
|
394
|
+
profile: 'compact',
|
|
395
|
+
});
|
|
353
396
|
// Progressive disclosure: primary path only. Full /ark-* catalog is expert depth
|
|
354
397
|
// (install via --skills-only). See docs/product-voice.md.
|
|
355
398
|
return `# Ark Enforcement
|
|
356
399
|
|
|
357
400
|
<!-- arkgate:compact-router host=${selectedHost} -->
|
|
401
|
+
${projectionBlock}
|
|
358
402
|
## Compact router
|
|
359
403
|
|
|
360
404
|
**Primary path (do this):**
|
|
361
405
|
|
|
362
|
-
1. Status anytime: \`${doctorCmd}\` — one status light, one next action (control plane).
|
|
363
|
-
2.
|
|
364
|
-
3.
|
|
365
|
-
4.
|
|
406
|
+
1. Status anytime: \`${doctorCmd}\` — one status light, one primary next action (control plane).
|
|
407
|
+
2. Read the **Improvement compass** section (not a score). Name residual lenses in plain language when present (SoC, DIP, domain, …). Out-of-scope lenses (performance, app security tooling, full resilience) stay honest — do not invent Ark enforcement for them.
|
|
408
|
+
3. Before trusting MCP evidence: call \`ark_identity\` with \`project.expectedRoot\` set to this project's exact absolute root, then reuse that root plus the returned \`projectIdentity.projectId\` on every Ark MCP call. A descendant path is authoritative only with that matching id. Missing tool, non-\`matched\` binding, or wrong root means the process is stale: restart the host and use the local CLI meanwhile.
|
|
409
|
+
4. Day to day: call \`ark_manifest\` with the same project expectation; place new files with \`ark_place\`; validate after edits; run \`${checkCmd}\`. The \`ark://manifest\` resource is compatibility-only and always unverified/non-authoritative. On a gate deny, fix the architecture — do not weaken the contract.
|
|
410
|
+
5. If MCP is unavailable: inspect \`ark.config.json\` and run \`${checkCmd}\`.
|
|
411
|
+
|
|
412
|
+
**Single door when residual remains:**
|
|
413
|
+
- **Edges debt** (import/capability violations) → fix with the gate / plan; skill pack only if doctor names a skill.
|
|
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
|
+
- Empty plan A + residual lenses / design-weak → **not finished**.
|
|
366
416
|
|
|
367
417
|
The selected host is \`${selectedHost}\`. Host registration and CI are installed with this file.
|
|
368
418
|
This compact router is enough for normal feature work.
|
|
@@ -370,7 +420,7 @@ This compact router is enough for normal feature work.
|
|
|
370
420
|
## Expert depth (optional)
|
|
371
421
|
|
|
372
422
|
Full \`/ark-*\` skills (including guided end-to-end \`/ark-autopilot\`) are **not** the default
|
|
373
|
-
curriculum. Install them only when doctor top action #1 or a STOP handoff names a skill:
|
|
423
|
+
curriculum. Install them only when doctor top action #1, residual compass, or a STOP handoff names a skill:
|
|
374
424
|
|
|
375
425
|
\`${installSkills}\`
|
|
376
426
|
`;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* Canonical algorithm: src/domain/diagnosticCatalog.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/diagnostic-catalog.mjs). Zero Node I/O.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Product-relative docs path (shipped in the npm tarball when listed in package files). */
|
|
12
|
+
export const DIAGNOSTIC_DOCS_RELATIVE_PATH = 'docs/diagnostics.md';
|
|
13
|
+
/** Schema id for serializing the catalog snapshot (agents / install projection). */
|
|
14
|
+
export const DIAGNOSTIC_CATALOG_SCHEMA_VERSION = '1.0';
|
|
15
|
+
function entry(ruleId, category, title, why, fix, extras) {
|
|
16
|
+
return {
|
|
17
|
+
ruleId,
|
|
18
|
+
title,
|
|
19
|
+
why,
|
|
20
|
+
fix,
|
|
21
|
+
docsAnchor: ruleId,
|
|
22
|
+
category,
|
|
23
|
+
...(extras?.oftenAdvisory ? { oftenAdvisory: true } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Closed public catalog. Order is stable (category groups, then ruleId) for
|
|
28
|
+
* deterministic serialization and docs generation.
|
|
29
|
+
*
|
|
30
|
+
* Every production-emitted `ruleId` must appear here. Remediation switch cases
|
|
31
|
+
* and adapter nextAction branches are parity-tested against this list.
|
|
32
|
+
*/
|
|
33
|
+
export const DIAGNOSTIC_CATALOG = Object.freeze([
|
|
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.', 'Define a port in the source layer, inject the outer-layer implementation, or move/share the type with `import type` when the edge is type-only — then preflight again. Do not weaken the layer rule without a hash-bound policy acknowledgement.'),
|
|
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
|
+
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
|
+
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.'),
|
|
39
|
+
// ── capability / ambient ─────────────────────────────────────────────────
|
|
40
|
+
entry('FORBIDDEN_GLOBAL', 'capability', 'Forbidden ambient global or dual import', 'The file’s layer lists this ambient (or its exact import dual, e.g. process / node:process) in forbiddenGlobals. Pure layers must not reach wall-clock, network, process, or similar effects directly.', 'Inject the capability through a small port (Clock, HttpPort, Config, …), bind the implementation outside the walled layer, then preflight again.'),
|
|
41
|
+
entry('CAPABILITY_VIOLATION', 'capability', 'Denied effect capability', 'The layer denies an effect capability (network, filesystem, clock, randomness, environment, process, persistence) and the candidate uses that effect via ambient or import evidence.', 'Define a capability port in the walled layer, bind the implementation in an adapter layer, then preflight again. Never mechanical-safe — port shape is a design decision.'),
|
|
42
|
+
// ── publish / intents ────────────────────────────────────────────────────
|
|
43
|
+
entry('RAW_EVENT_PUBLISH', 'publish', 'Raw event publish', 'Publish went through a raw string or object instead of a registered intent creator, bypassing Ark intent contracts and tooling.', 'Publish through a registered intent creator, then run Ark again.'),
|
|
44
|
+
entry('PUBLISH_MISSING_SOURCE', 'publish', 'Publish missing metadata.source', 'A strict Ark publish call omitted metadata.source, so the publishing layer cannot be verified.', 'Add metadata.source to the publish call, then run Ark again.'),
|
|
45
|
+
entry('PUBLISH_SOURCE_LAYER_MISMATCH', 'publish', 'Publish source layer mismatch', 'metadata.source resolves to a different layer than the file performing the publish.', 'Use a source intent owned by the same layer as this file, or move the publish call to the owning layer.'),
|
|
46
|
+
entry('UNKNOWN_INTENT', 'publish', 'Unknown intent reference', 'Snippet analysis saw an intent string that is not registered in the intent registry / profile under check.', 'Register the intent or use a known intent name from the project registry, then re-run the gate.'),
|
|
47
|
+
// ── safety thresholds / dynamic ──────────────────────────────────────────
|
|
48
|
+
entry('DYNAMIC_IMPORT_NOT_ALLOWLISTED', 'safety', 'Non-literal dynamic import', 'A dynamic import(expr) cannot be resolved statically and the file is not on dynamicImportAllowlist. Unresolved dynamics can hide layer edges.', 'Rewrite to a static import when possible, or add only reviewed files to dynamicImportAllowlist after human sign-off.'),
|
|
49
|
+
entry('DYNAMIC_REQUIRE_NOT_ALLOWLISTED', 'safety', 'Non-literal require', 'A require(expr) cannot be resolved statically and is not allowlisted — same hide-the-edge risk as dynamic import.', 'Prefer static import, or allowlist only reviewed files after sign-off.'),
|
|
50
|
+
entry('TS_SUPPRESSION_THRESHOLD_EXCEEDED', 'safety', '@ts-ignore / @ts-nocheck threshold', 'Count of TypeScript suppressions in governed production source exceeds safety.maxTsSuppressions.', 'Remove suppressions by fixing types, or raise the threshold only with an explicit production exception in ark.config.json.'),
|
|
51
|
+
entry('ANY_CAST_THRESHOLD_EXCEEDED', 'safety', 'Explicit any cast threshold', 'Count of explicit any casts exceeds safety.maxAnyCasts.', 'Replace any with precise types, or raise the threshold only with a documented exception.'),
|
|
52
|
+
entry('IN_MEMORY_STORE_IN_PRODUCTION_SOURCE', 'safety', 'In-memory store in production source', 'Governed production source references an Ark InMemory* store without safety.allowInMemory — durable systems should not ship ephemeral stores by accident.', 'Provide a durable store implementation, or set safety.allowInMemory only for an explicitly ephemeral service.'),
|
|
53
|
+
entry('PEER_ISOLATION_DISABLED', 'safety', 'peerIsolation disabled on a rule', 'A same-layer or peer rule disables peerIsolation (or omits it where required), which allows cross-slice coupling the contract otherwise blocks.', 'Restore peerIsolation: true, or set safety.allowDisabledPeerIsolation only with a documented production exception.'),
|
|
54
|
+
// ── ArkRules ─────────────────────────────────────────────────────────────
|
|
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
|
+
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
|
+
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.'),
|
|
59
|
+
// ── atomic preflight / change set ────────────────────────────────────────
|
|
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
|
+
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.'),
|
|
62
|
+
entry('DELETE_TARGET_MISSING', 'preflight', 'Delete target missing', 'A delete operation targets a path that is not present in the supplied base tree.', 'Remove the delete, or include the file in the base tree facts, then preflight again.'),
|
|
63
|
+
entry('CHANGE_SET_EMPTY', 'preflight', 'Empty change set', 'Atomic preflight was invoked with no create, update, or delete operations.', 'Provide at least one change operation, then preflight again.'),
|
|
64
|
+
entry('FACTS_IDENTITY_MISMATCH', 'preflight', 'Base/candidate facts identity mismatch', 'Base and candidate resolved facts disagree on resolver, compiler, evidence requirements, or package identity — verdicts would not be comparable.', 'Regenerate both fact snapshots with the same resolver/compiler/evidence requirements, then preflight again.'),
|
|
65
|
+
entry('CANDIDATE_DELETE_NOT_APPLIED', 'preflight', 'Candidate still contains deleted path', 'Facts claim a delete, but the candidate tree still includes the path.', 'Ensure the candidate facts apply the delete (path absent), then preflight again.'),
|
|
66
|
+
entry('CANDIDATE_CHANGE_MISSING', 'preflight', 'Declared change missing from candidate', 'The change set declares a create/update whose path is missing from candidate facts.', 'Include the new content in the candidate facts (or drop the operation), then preflight again.'),
|
|
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
|
+
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
|
+
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 (e.g. domain-logic-in-ui) under --fail-on-new-smells.', 'Revert the regression or redesign so the smell does not worsen versus base, then re-run with the same base ref.'),
|
|
71
|
+
// ── analysis completeness / host ─────────────────────────────────────────
|
|
72
|
+
entry('ANALYSIS_PARSE_INCOMPLETE', 'analysis', 'Parse incomplete', 'Governed source could not be fully parsed; analysis is partial and must not paint green.', 'Fix syntax/parse errors in governed files (or restore a usable TypeScript host), then re-run. Partial never means pass.'),
|
|
73
|
+
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
|
+
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
|
+
// ── AI snippet gate policy surface ───────────────────────────────────────
|
|
76
|
+
entry('FORBIDDEN_PATTERN', 'snippet-policy', 'Forbidden regex pattern', 'Snippet content matched a project or profile forbiddenPatterns rule.', 'Remove or rewrite the matching code so the pattern no longer matches, then re-run the snippet gate.'),
|
|
77
|
+
entry('FORBIDDEN_SUBSTRING', 'snippet-policy', 'Forbidden substring', 'Snippet content contained a forbidden substring from the AI gate options/profile.', 'Remove the forbidden substring, then re-run the snippet gate.'),
|
|
78
|
+
entry('FORBIDDEN_IMPORT', 'snippet-policy', 'Forbidden import target', 'Snippet imported or required a module listed as forbidden for the active profile.', 'Import an allowed module or inject the dependency behind a port, then re-run.'),
|
|
79
|
+
entry('POLICY_VIOLATION', 'snippet-policy', 'Policy engine violation', 'A registered Policy failed on the snippet or generated code under evaluation.', 'Adjust the code to satisfy the named policy, or change the policy only through an explicit contract decision.'),
|
|
80
|
+
entry('EXTENSION_ERROR', 'snippet-policy', 'AI gate extension error', 'A registered AICodeGate extension threw while analyzing the snippet.', 'Fix or remove the failing extension; do not ignore extension failures as pass.'),
|
|
81
|
+
entry('AST_ANALYZER_ERROR', 'snippet-policy', 'AST analyzer error', 'Built-in AST/symbol analysis failed (host error or unexpected analyzer exception).', 'Ensure TypeScript host and snippet are valid; re-run. If the analyzer crashes on valid input, file a bug with a minimal fixture.'),
|
|
82
|
+
// ── config diagnostics ───────────────────────────────────────────────────
|
|
83
|
+
entry('CONFIG_INVALID_DYNAMIC_IMPORT_ALLOWLIST', 'config', 'Invalid dynamicImportAllowlist', 'dynamicImportAllowlist is present but not an array of file globs.', 'Set dynamicImportAllowlist to an array of project-relative globs (or omit it).', { oftenAdvisory: true }),
|
|
84
|
+
entry('CONFIG_INVALID_SAFETY', 'config', 'Invalid safety object', 'The safety field is present but not an object.', 'Use a safety object with optional maxTsSuppressions, maxAnyCasts, allowInMemory, allowDisabledPeerIsolation.', { oftenAdvisory: true }),
|
|
85
|
+
entry('CONFIG_INVALID_SAFETY_THRESHOLD', 'config', 'Invalid safety threshold', 'A safety threshold (maxTsSuppressions / maxAnyCasts) is not a non-negative integer.', 'Set each threshold to a non-negative integer.', { oftenAdvisory: true }),
|
|
86
|
+
entry('CONFIG_NO_LAYERS', 'config', 'No layers configured', 'ark.config.json has no file layers, so import-boundary enforcement cannot classify files.', 'Declare at least one layer with name + patterns (or run ark start / a preset).', { oftenAdvisory: true }),
|
|
87
|
+
entry('CONFIG_LAYER_WITHOUT_NAME', 'config', 'Layer missing name', 'A configured layer entry has no name.', 'Give every layer a unique non-empty name.', { oftenAdvisory: true }),
|
|
88
|
+
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
|
+
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
|
+
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 so governed files match.', { oftenAdvisory: true }),
|
|
92
|
+
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
|
+
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
|
+
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 }),
|
|
95
|
+
entry('CONFIG_AMBIGUOUS_LAYERS', 'config', 'Ambiguous layer classification', 'Some files match multiple layers at equal specificity; classification falls back to declaration order.', 'Disambiguate overlapping patterns so each file has one clear layer owner.', { oftenAdvisory: true }),
|
|
96
|
+
entry('CONFIG_UNCLASSIFIED_FILES', 'config', 'Unclassified included files', 'Included source files match no layer pattern; import rules will not enforce on them.', 'Extend layer patterns or narrow include so every governed file is classified.', { oftenAdvisory: true }),
|
|
97
|
+
// ── meta ─────────────────────────────────────────────────────────────────
|
|
98
|
+
entry('ARK_UNKNOWN', 'meta', 'Unknown diagnostic', 'A diagnostic lacked a stable ruleId/code; adapters may emit this fallback so agents never see an empty id.', 'Resolve the underlying finding without weakening ark.config.json, then run Ark again. Prefer fixing the producer to emit a catalogued ruleId.'),
|
|
99
|
+
]);
|
|
100
|
+
const BY_ID = new Map(DIAGNOSTIC_CATALOG.map((item) => [item.ruleId, item]));
|
|
101
|
+
/** All public ruleIds in catalog order. */
|
|
102
|
+
export const DIAGNOSTIC_RULE_IDS = Object.freeze(DIAGNOSTIC_CATALOG.map((item) => item.ruleId));
|
|
103
|
+
export function isKnownDiagnosticCode(ruleId) {
|
|
104
|
+
return typeof ruleId === 'string' && ruleId.length > 0 && BY_ID.has(ruleId);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* True when the code is catalogued or is an ArkRule-family id handled by the
|
|
108
|
+
* ARKRULE_* prefix fallback in remediation (structure sensors may add members later
|
|
109
|
+
* only via catalog + ROADMAP — prefix alone is not a license for free-form ids).
|
|
110
|
+
*/
|
|
111
|
+
export function isCataloguedOrArkRuleFamily(ruleId) {
|
|
112
|
+
if (typeof ruleId !== 'string' || ruleId.length === 0)
|
|
113
|
+
return false;
|
|
114
|
+
if (BY_ID.has(ruleId))
|
|
115
|
+
return true;
|
|
116
|
+
return ruleId.startsWith('ARKRULE_');
|
|
117
|
+
}
|
|
118
|
+
export function getDiagnosticCatalogEntry(ruleId) {
|
|
119
|
+
if (typeof ruleId !== 'string' || ruleId.length === 0)
|
|
120
|
+
return undefined;
|
|
121
|
+
return BY_ID.get(ruleId);
|
|
122
|
+
}
|
|
123
|
+
/** Fragment for docs links, e.g. `#LAYER_IMPORT_VIOLATION`. */
|
|
124
|
+
export function diagnosticDocsFragment(ruleId) {
|
|
125
|
+
const entryOrId = getDiagnosticCatalogEntry(ruleId)?.docsAnchor ?? ruleId;
|
|
126
|
+
return `#${entryOrId}`;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Repo-relative docs path with fragment (for agents and JSON).
|
|
130
|
+
* Package consumers resolve against the installed package root or GitHub tree.
|
|
131
|
+
*/
|
|
132
|
+
export function diagnosticDocsPath(ruleId) {
|
|
133
|
+
return `${DIAGNOSTIC_DOCS_RELATIVE_PATH}${diagnosticDocsFragment(ruleId)}`;
|
|
134
|
+
}
|
|
135
|
+
/** Catalog snapshot for JSON export / agent projection (stable field order). */
|
|
136
|
+
export function serializeDiagnosticCatalog() {
|
|
137
|
+
return {
|
|
138
|
+
schemaVersion: DIAGNOSTIC_CATALOG_SCHEMA_VERSION,
|
|
139
|
+
docsPath: DIAGNOSTIC_DOCS_RELATIVE_PATH,
|
|
140
|
+
codes: DIAGNOSTIC_CATALOG,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Static catalog fix for a ruleId when no live violation context is available.
|
|
145
|
+
* Live adapters should still use deterministicNextAction for specialized edges.
|
|
146
|
+
*/
|
|
147
|
+
export function catalogFixForRuleId(ruleId) {
|
|
148
|
+
return getDiagnosticCatalogEntry(ruleId)?.fix;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Static catalog why for a ruleId (agent “why” surface).
|
|
152
|
+
*/
|
|
153
|
+
export function catalogWhyForRuleId(ruleId) {
|
|
154
|
+
return getDiagnosticCatalogEntry(ruleId)?.why;
|
|
155
|
+
}
|
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -62,6 +62,10 @@ import { computeDoctorAdvisories, printDoctorAdvisories } from './doctor-advisor
|
|
|
62
62
|
import { ANALYSIS_COMPLETENESS, analysisIncompleteStatement, normalizeAnalysisCompleteness } from './analysis-completeness.mjs';
|
|
63
63
|
import { designDeltaDoctorLines } from './design-delta.mjs';
|
|
64
64
|
import { enforcementDoctorLines } from './enforcement-state.mjs';
|
|
65
|
+
import {
|
|
66
|
+
buildDoctorImprovementCompass,
|
|
67
|
+
printImprovementCompassSection,
|
|
68
|
+
} from './improvement-compass-doctor.mjs';
|
|
65
69
|
|
|
66
70
|
const color = {
|
|
67
71
|
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
@@ -640,6 +644,23 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
640
644
|
packageVersionTruth?.code === 'PACKAGE_PIN_SELF_HOST',
|
|
641
645
|
});
|
|
642
646
|
|
|
647
|
+
// Improvement compass: projection only — never feeds ok/valid/goal.met.
|
|
648
|
+
const improvementCompass = buildDoctorImprovementCompass({
|
|
649
|
+
designSmells,
|
|
650
|
+
violations,
|
|
651
|
+
designWeak: designFitness.designWeak === true,
|
|
652
|
+
physicalCohesion: doctorAdvisories.physicalCohesion,
|
|
653
|
+
rulesUnderContract,
|
|
654
|
+
baselineExists: baseline.exists,
|
|
655
|
+
baselineStale: analysisComplete ? staleBaseline : null,
|
|
656
|
+
frozenResidual: baseline.exists ? baseline.keys.size : null,
|
|
657
|
+
dirtyBaselineRisk: productHonesty?.reasonIds?.includes?.('dirty-baseline') === true,
|
|
658
|
+
ungovernedDirCount: cov.suggestions?.length ?? 0,
|
|
659
|
+
emptyLayerCount: cov.emptyLayers?.length ?? 0,
|
|
660
|
+
goldenPatternPresent: goldenPattern.present === true,
|
|
661
|
+
arkRulesLoaded: rulesUnderContract?.active === true,
|
|
662
|
+
});
|
|
663
|
+
|
|
643
664
|
if (asJson) {
|
|
644
665
|
(options.writeJson ?? console.log)(
|
|
645
666
|
JSON.stringify(
|
|
@@ -654,6 +675,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
654
675
|
// Path-correct ENFORCE can still be design-weak (P02).
|
|
655
676
|
designFitness,
|
|
656
677
|
designSmells,
|
|
678
|
+
// Improvement compass (lenses; notAScore; never a gate input).
|
|
679
|
+
improvementCompass,
|
|
657
680
|
...(options.designDelta ? { designDelta: options.designDelta } : {}),
|
|
658
681
|
// Q01: primary next action when Shape residual dominates (null if not design-weak).
|
|
659
682
|
postGreenPath,
|
|
@@ -846,6 +869,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
846
869
|
}
|
|
847
870
|
}
|
|
848
871
|
|
|
872
|
+
printImprovementCompassSection(improvementCompass, { line, warn, ok, color });
|
|
873
|
+
|
|
849
874
|
console.log('');
|
|
850
875
|
console.log(color.bold('Design fitness'));
|
|
851
876
|
if (designSmells.length === 0) {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { effectiveCapabilityDeny } from './analysis-engine.mjs';
|
|
11
11
|
import { graphBlindSpotsHtml } from './graph-blind.mjs';
|
|
12
12
|
import { formatRulesUnderContractHtml } from './rules-under-contract.mjs';
|
|
13
|
+
import { primaryImprovementCompassNextAction } from './improvement-compass.mjs';
|
|
13
14
|
|
|
14
15
|
// htmlEscape is injected by the caller (html-report.mjs) — importing it back
|
|
15
16
|
// would be a dependency cycle, and the repo's own gate blocks that. The
|
|
@@ -247,10 +248,42 @@ function rulesUnderContractHtml(section) {
|
|
|
247
248
|
return formatRulesUnderContractHtml(section, esc);
|
|
248
249
|
}
|
|
249
250
|
|
|
251
|
+
/** Improvement compass — advisory lenses only; never a score bar or gate input. */
|
|
252
|
+
function improvementCompassHtml(compass) {
|
|
253
|
+
if (!compass || compass.notAScore !== true || !Array.isArray(compass.lenses)) return '';
|
|
254
|
+
const residual = Array.isArray(compass.topResidual) ? compass.topResidual : [];
|
|
255
|
+
const residualLine =
|
|
256
|
+
residual.length === 0
|
|
257
|
+
? '<p class="muted">Residual: none on instrumented lenses (not a score — green edges ≠ finished design).</p>'
|
|
258
|
+
: `<p><span class="tag warn">residual</span> ${residual
|
|
259
|
+
.map((id) => {
|
|
260
|
+
const lens = compass.lenses.find((l) => l.id === id);
|
|
261
|
+
return `<code>${esc(id)}</code>${lens?.summary ? ` — ${esc(lens.summary)}` : ''}`;
|
|
262
|
+
})
|
|
263
|
+
.join('<br/>')}</p>`;
|
|
264
|
+
const oos = compass.lenses
|
|
265
|
+
.filter((l) => l && l.status === 'out-of-scope')
|
|
266
|
+
.map((l) => `<code>${esc(l.id)}</code>`)
|
|
267
|
+
.join(' · ');
|
|
268
|
+
// Same primary next as doctor human: severity-ordered topResidual, not lens-id order.
|
|
269
|
+
const next = primaryImprovementCompassNextAction(compass);
|
|
270
|
+
const nextLine = next
|
|
271
|
+
? `<p class="muted">Next: <code>${esc(next.ref)}</code> — ${esc(next.summary)}</p>`
|
|
272
|
+
: '';
|
|
273
|
+
return `
|
|
274
|
+
<section class="section card" data-advisory="improvementCompass">
|
|
275
|
+
<h2>Improvement compass <span class="muted">(not a score — projection only; never changes the verdict)</span></h2>
|
|
276
|
+
${residualLine}
|
|
277
|
+
<p class="muted">Out of scope (honest): ${oos || 'scalability · resilience · security'}</p>
|
|
278
|
+
${nextLine}
|
|
279
|
+
</section>`;
|
|
280
|
+
}
|
|
281
|
+
|
|
250
282
|
export function renderAdvisorySections(advisories, escape) {
|
|
251
283
|
if (!advisories || typeof advisories !== 'object') return '';
|
|
252
284
|
if (typeof escape === 'function') esc = escape;
|
|
253
285
|
return [
|
|
286
|
+
improvementCompassHtml(advisories.improvementCompass),
|
|
254
287
|
contractHealthHtml(advisories.contractHealth),
|
|
255
288
|
ambientStateHtml(advisories.ambientState),
|
|
256
289
|
physicalCohesionHtml(advisories.physicalCohesion),
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
|
|
22
22
|
import { readBaseline, baselineOccurrenceKeys } from './violations.mjs';
|
|
23
23
|
import { describePackageVersionDualTruth } from './field-install.mjs';
|
|
24
|
+
import { buildDoctorImprovementCompass } from './improvement-compass-doctor.mjs';
|
|
25
|
+
import { computePhysicalCohesion } from './physical-cohesion.mjs';
|
|
24
26
|
|
|
25
27
|
function esc(value) {
|
|
26
28
|
return String(value)
|
|
@@ -43,6 +45,7 @@ function esc(value) {
|
|
|
43
45
|
* frozenKeys?: number,
|
|
44
46
|
* activeCount?: number,
|
|
45
47
|
* activeBlockingCount?: number,
|
|
48
|
+
* baselineStale?: number | null,
|
|
46
49
|
* }} [baselineSplit] same numbers doctor uses (do not recompute from active-only list)
|
|
47
50
|
*/
|
|
48
51
|
export function buildReportDepthPayload(
|
|
@@ -155,6 +158,26 @@ export function buildReportDepthPayload(
|
|
|
155
158
|
primaryNextAction: postGreenPath?.action ?? dualTruthNext,
|
|
156
159
|
activeBlockingViolations: activeBlockingCount,
|
|
157
160
|
});
|
|
161
|
+
// Doctor parity: same physical-cohesion + baseline stale facts as runDoctor.
|
|
162
|
+
const physicalCohesion = computePhysicalCohesion(root, files);
|
|
163
|
+
const baselineStale =
|
|
164
|
+
typeof baselineSplit.baselineStale === 'number' ? baselineSplit.baselineStale : null;
|
|
165
|
+
// Improvement compass — same projection as doctor; notAScore; never a gate input.
|
|
166
|
+
const improvementCompass = buildDoctorImprovementCompass({
|
|
167
|
+
designSmells,
|
|
168
|
+
violations: activeViolations,
|
|
169
|
+
designWeak: designFitness.designWeak === true,
|
|
170
|
+
physicalCohesion,
|
|
171
|
+
rulesUnderContract,
|
|
172
|
+
baselineExists: baseline.exists || frozenKeys > 0,
|
|
173
|
+
baselineStale,
|
|
174
|
+
frozenResidual: frozenKeys,
|
|
175
|
+
dirtyBaselineRisk: productHonesty?.reasonIds?.includes?.('dirty-baseline') === true,
|
|
176
|
+
ungovernedDirCount: coverage?.suggestions?.length ?? 0,
|
|
177
|
+
emptyLayerCount: coverage?.emptyLayers?.length ?? 0,
|
|
178
|
+
goldenPatternPresent: goldenPattern.present === true,
|
|
179
|
+
arkRulesLoaded: rulesUnderContract?.active === true,
|
|
180
|
+
});
|
|
158
181
|
return {
|
|
159
182
|
adoption,
|
|
160
183
|
designDepth: {
|
|
@@ -166,6 +189,7 @@ export function buildReportDepthPayload(
|
|
|
166
189
|
// P0-B / P1-M — folded into designDepth so --report stays a single payload.
|
|
167
190
|
productHonesty,
|
|
168
191
|
mergePlanes: rulesUnderContract?.mergePlanes ?? null,
|
|
192
|
+
improvementCompass,
|
|
169
193
|
},
|
|
170
194
|
};
|
|
171
195
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor adapter for the Domain improvement compass (notAScore projection).
|
|
3
|
+
* Keeps doctor-plan.mjs inside its module budget; pure assembly only.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
buildImprovementCompass,
|
|
7
|
+
formatImprovementCompassDoctorLines,
|
|
8
|
+
primaryImprovementCompassNextAction,
|
|
9
|
+
} from './improvement-compass.mjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {{
|
|
13
|
+
* designSmells?: object[],
|
|
14
|
+
* violations?: object[],
|
|
15
|
+
* designWeak?: boolean,
|
|
16
|
+
* physicalCohesion?: { findings?: object[] } | null,
|
|
17
|
+
* rulesUnderContract?: object | null,
|
|
18
|
+
* baselineExists?: boolean,
|
|
19
|
+
* baselineStale?: number | null,
|
|
20
|
+
* frozenResidual?: number | null,
|
|
21
|
+
* dirtyBaselineRisk?: boolean,
|
|
22
|
+
* ungovernedDirCount?: number,
|
|
23
|
+
* emptyLayerCount?: number,
|
|
24
|
+
* goldenPatternPresent?: boolean,
|
|
25
|
+
* arkRulesLoaded?: boolean,
|
|
26
|
+
* }} input
|
|
27
|
+
*/
|
|
28
|
+
export function buildDoctorImprovementCompass(input = {}) {
|
|
29
|
+
const violations = Array.isArray(input.violations) ? input.violations : [];
|
|
30
|
+
const ruleId = (v) => String(v?.ruleId ?? v?.code ?? '');
|
|
31
|
+
|
|
32
|
+
let cycleCount = 0;
|
|
33
|
+
let peerIsolationCount = 0;
|
|
34
|
+
let pureOrCapabilityResidual = 0;
|
|
35
|
+
let forbiddenGlobalResidual = 0;
|
|
36
|
+
let arkRulesStructureResidual = 0;
|
|
37
|
+
|
|
38
|
+
for (const v of violations) {
|
|
39
|
+
const id = ruleId(v).toUpperCase();
|
|
40
|
+
if (!id) continue;
|
|
41
|
+
if (id.includes('CYCLE') || id === 'CIRCULAR_DEPENDENCY') cycleCount += 1;
|
|
42
|
+
if (id.includes('PEER_ISOLATION')) peerIsolationCount += 1;
|
|
43
|
+
if (id === 'CAPABILITY_VIOLATION') pureOrCapabilityResidual += 1;
|
|
44
|
+
if (id === 'FORBIDDEN_GLOBAL' || id.startsWith('FORBIDDEN_')) forbiddenGlobalResidual += 1;
|
|
45
|
+
if (id.startsWith('ARKRULE_') || id === 'INVARIANT_UNCOVERED') arkRulesStructureResidual += 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const pcFindings = input.physicalCohesion?.findings;
|
|
49
|
+
const physicalCohesionFindingCount = Array.isArray(pcFindings) ? pcFindings.length : 0;
|
|
50
|
+
|
|
51
|
+
const arkRulesLoaded =
|
|
52
|
+
input.arkRulesLoaded === true ||
|
|
53
|
+
input.rulesUnderContract?.active === true ||
|
|
54
|
+
(typeof input.rulesUnderContract?.structureRules === 'number' &&
|
|
55
|
+
input.rulesUnderContract.structureRules > 0);
|
|
56
|
+
|
|
57
|
+
return buildImprovementCompass({
|
|
58
|
+
designSmells: Array.isArray(input.designSmells) ? input.designSmells : [],
|
|
59
|
+
violations: violations.map((v) => ({
|
|
60
|
+
ruleId: ruleId(v) || undefined,
|
|
61
|
+
message: typeof v?.message === 'string' ? v.message : undefined,
|
|
62
|
+
file: typeof v?.file === 'string' ? v.file : typeof v?.path === 'string' ? v.path : undefined,
|
|
63
|
+
fromLayer: v?.fromLayer,
|
|
64
|
+
toLayer: v?.toLayer,
|
|
65
|
+
failsStrict: v?.failsStrict,
|
|
66
|
+
typeOnly: v?.typeOnly === true || v?.namedBindingsTypeOnly === true || undefined,
|
|
67
|
+
})),
|
|
68
|
+
cycleCount,
|
|
69
|
+
peerIsolationCount,
|
|
70
|
+
physicalCohesionFindingCount,
|
|
71
|
+
arkRulesLoaded,
|
|
72
|
+
arkRulesStructureResidual,
|
|
73
|
+
designWeak: input.designWeak === true,
|
|
74
|
+
baselineExists: input.baselineExists === true,
|
|
75
|
+
baselineStale: input.baselineStale ?? null,
|
|
76
|
+
frozenResidual: input.frozenResidual ?? null,
|
|
77
|
+
dirtyBaselineRisk: input.dirtyBaselineRisk === true,
|
|
78
|
+
pureOrCapabilityResidual,
|
|
79
|
+
forbiddenGlobalResidual,
|
|
80
|
+
ungovernedDirCount: Number(input.ungovernedDirCount) || 0,
|
|
81
|
+
emptyLayerCount: Number(input.emptyLayerCount) || 0,
|
|
82
|
+
goldenPatternPresent: input.goldenPatternPresent === true,
|
|
83
|
+
// Doctor path is TypeScript-oriented (ArkGate product surface).
|
|
84
|
+
stackKind: 'typescript',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export {
|
|
89
|
+
formatImprovementCompassDoctorLines,
|
|
90
|
+
primaryImprovementCompassNextAction,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Human doctor section (never a score bar).
|
|
95
|
+
* @param {import('./improvement-compass.mjs').ImprovementCompass} compass
|
|
96
|
+
* @param {{ line: Function, warn: string, ok: string, color: { bold: Function } }} io
|
|
97
|
+
*/
|
|
98
|
+
export function printImprovementCompassSection(compass, io) {
|
|
99
|
+
const { line, warn, ok, color } = io;
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log(color.bold('Improvement compass (not a score)'));
|
|
102
|
+
const mark = compass.topResidual.length > 0 ? warn : ok;
|
|
103
|
+
for (const text of formatImprovementCompassDoctorLines(compass)) {
|
|
104
|
+
line(mark, text);
|
|
105
|
+
}
|
|
106
|
+
}
|