arkgate 4.8.0 → 4.8.2

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 (49) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +5 -3
  3. package/bin/lib/analysis-engine.mjs +4 -4
  4. package/bin/lib/architecture-scan.mjs +7 -2
  5. package/bin/lib/invariant-coverage-io.mjs +61 -24
  6. package/bin/lib/invariant-coverage.mjs +4 -1
  7. package/bin/lib/policy-delta-io.mjs +8 -2
  8. package/bin/lib/rules-under-contract.mjs +8 -2
  9. package/dist/{diagnosticCatalog-RiKPUFRG.d.ts → diagnosticCatalog-CPzH-MLN.d.ts} +7 -1
  10. package/dist/index.cjs +6 -6
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.js +6 -6
  13. package/dist/nestjs/index.cjs +1 -1
  14. package/dist/nestjs/index.js +1 -1
  15. package/dist/runtime/index.cjs +15 -15
  16. package/dist/runtime/index.d.ts +1 -1
  17. package/dist/runtime/index.js +15 -15
  18. package/docs/README.md +2 -2
  19. package/docs/agent-guide.md +10 -9
  20. package/docs/package-surface.md +3 -1
  21. package/package.json +1 -1
  22. package/server.json +2 -2
  23. package/templates/agent-skills/README.md +1 -1
  24. package/templates/agent-skills/ark-adopt/SKILL.md +41 -13
  25. package/templates/agent-skills/ark-architect/SKILL.md +4 -4
  26. package/templates/agent-skills/ark-autopilot/SKILL.md +22 -4
  27. package/templates/agent-skills/ark-contract/SKILL.md +7 -7
  28. package/templates/agent-skills/ark-coverage/SKILL.md +11 -5
  29. package/templates/agent-skills/ark-explain/SKILL.md +10 -4
  30. package/templates/agent-skills/ark-explore/SKILL.md +12 -4
  31. package/templates/agent-skills/ark-fix/SKILL.md +3 -3
  32. package/templates/agent-skills/ark-loop/SKILL.md +3 -3
  33. package/templates/agent-skills/ark-place/SKILL.md +12 -3
  34. package/templates/agent-skills/ark-runtime/SKILL.md +13 -11
  35. package/templates/agent-skills/ark-think/SKILL.md +13 -7
  36. package/templates/agent-skills/ark-upgrade/SKILL.md +12 -4
  37. package/templates/skills/ark-adopt.md +41 -13
  38. package/templates/skills/ark-architect.md +4 -4
  39. package/templates/skills/ark-autopilot.md +22 -4
  40. package/templates/skills/ark-contract.md +7 -7
  41. package/templates/skills/ark-coverage.md +11 -5
  42. package/templates/skills/ark-explain.md +10 -4
  43. package/templates/skills/ark-explore.md +12 -4
  44. package/templates/skills/ark-fix.md +3 -3
  45. package/templates/skills/ark-loop.md +3 -3
  46. package/templates/skills/ark-place.md +12 -3
  47. package/templates/skills/ark-runtime.md +13 -11
  48. package/templates/skills/ark-think.md +13 -7
  49. package/templates/skills/ark-upgrade.md +12 -4
@@ -11,7 +11,10 @@ import {
11
11
  import { effectiveAnalysisConfig } from './analysis-policy.mjs';
12
12
  import { resolveCandidateFacts } from './resolved-candidate-facts.mjs';
13
13
  import { loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
14
- import { loadInvariantCoverageInputs } from './invariant-coverage-io.mjs';
14
+ import {
15
+ invariantIdsFromCatalog,
16
+ loadInvariantCoverageInputs,
17
+ } from './invariant-coverage-io.mjs';
15
18
  import { loadArkRuleFileHints } from './arkrule-file-hints.mjs';
16
19
 
17
20
  /** Resolve canonical facts and optionally retain filesystem probes for resident invalidation. */
@@ -67,7 +70,9 @@ export function resolveArchitectureSnapshot({
67
70
  });
68
71
  const hasInvariants = (arkRulesLoad.arkRules?.invariants?.length ?? 0) > 0;
69
72
  const coverageInputs = hasInvariants
70
- ? loadInvariantCoverageInputs(root, facts)
73
+ ? loadInvariantCoverageInputs(root, facts, {
74
+ invariantIds: invariantIdsFromCatalog(arkRulesLoad.arkRules),
75
+ })
71
76
  : undefined;
72
77
  // AR07: Tooling fileHints for orchestration-only / thin-adapter (reuse coverage contents when present).
73
78
  const fileHints = loadArkRuleFileHints(
@@ -62,16 +62,42 @@ function matchSimpleGlob(glob, file) {
62
62
  return new RegExp(`^${out}$`).test(target);
63
63
  }
64
64
 
65
+ /**
66
+ * Declared invariant ids from an Effective catalog. Empty when the extra is off.
67
+ * @param {{ invariants?: Array<{ id?: unknown }> } | null | undefined} arkRules
68
+ * @returns {string[]}
69
+ */
70
+ export function invariantIdsFromCatalog(arkRules) {
71
+ return (arkRules?.invariants ?? [])
72
+ .map((inv) => inv?.id)
73
+ .filter((id) => typeof id === 'string' && id.length > 0);
74
+ }
75
+
65
76
  /**
66
77
  * @param {string} root
67
78
  * @param {{ files?: Array<{ path: string }> }} facts
68
- * @param {{ testGlobs?: string[] }} [opts]
69
- * @returns {{ fileContents: Record<string, string>, testFiles: string[], testGlobsMissing: boolean }}
79
+ * @param {{ testGlobs?: string[], invariantIds?: string[] }} [opts]
80
+ * @returns {{
81
+ * fileContents: Record<string, string>,
82
+ * testFiles: string[],
83
+ * testGlobsMissing: boolean,
84
+ * coverageBudgetExhausted: boolean,
85
+ * }}
70
86
  */
71
87
  export function loadInvariantCoverageInputs(root, facts, opts = {}) {
72
88
  const fileContents = {};
73
89
  const testFiles = [];
74
90
  const seen = new Set();
91
+ // Declared invariant ids. When present, a test file is RETAINED only if it
92
+ // mentions one: scanning is cheap (hundreds of small files), retaining is
93
+ // what costs memory. Without this the budget goes to whichever N tests the
94
+ // walk reaches first — an arbitrary order — so coverage is wrong on any repo
95
+ // with more test files than budget. Measured: 707 tests against a cap of 400.
96
+ const invariantIds = Array.isArray(opts.invariantIds)
97
+ ? opts.invariantIds.filter((id) => typeof id === 'string' && id.length > 0)
98
+ : [];
99
+ const mentionsInvariant = (content) =>
100
+ invariantIds.length === 0 || invariantIds.some((id) => content.includes(id));
75
101
  const testGlobs = Array.isArray(opts.testGlobs)
76
102
  ? opts.testGlobs.filter((g) => typeof g === 'string' && g.length > 0)
77
103
  : [];
@@ -93,40 +119,51 @@ export function loadInvariantCoverageInputs(root, facts, opts = {}) {
93
119
  const stat = fs.statSync(absolute);
94
120
  if (!stat.isFile() || stat.size > MAX_FILE_BYTES) return;
95
121
  const content = fs.readFileSync(absolute, 'utf8');
122
+ const asTest = forceAsTest || isTestPath(rel);
123
+ // A test that names no invariant is evidence of nothing: scan it, drop
124
+ // it, and let it cost no budget.
125
+ if (asTest && !mentionsInvariant(content)) return;
96
126
  seen.add(rel);
97
127
  fileContents[rel] = content;
98
- if (forceAsTest || isTestPath(rel)) testFiles.push(rel);
128
+ if (asTest) testFiles.push(rel);
99
129
  } catch {
100
130
  // skip unreadable
101
131
  }
102
132
  };
103
133
 
104
- for (const file of facts?.files ?? []) {
105
- if (file?.path) pushFile(file.path);
134
+ // Tests FIRST, then production files.
135
+ //
136
+ // The order is load-bearing, not stylistic. `pushFile` stops at
137
+ // MAX_COVERAGE_FILES, and a real repo has far more production files than the
138
+ // budget — so walking facts first consumed the whole budget and the test walk
139
+ // pushed nothing. Coverage then reported `testGlobsMissing: true`, which the
140
+ // caller renders as "never-had-tests": a claim about the USER's repo that was
141
+ // actually about our own budget. Measured on a 4511-file project: every
142
+ // invariant reported uncovered while its test sat on disk with the invariant
143
+ // id in the describe title. Tests are tens of files, not thousands, so giving
144
+ // them the head of the budget costs the production scan nothing in practice.
145
+ const testWalkRoots = useCustomGlobs
146
+ ? ['.', 'tests', 'test', 'src', '__tests__', 'spec']
147
+ : ['tests', 'test', 'src', '__tests__'];
148
+ for (const dir of testWalkRoots) {
149
+ const absDir = path.join(root, dir === '.' ? '' : dir);
150
+ if (!fs.existsSync(absDir)) continue;
151
+ walkTestFiles(absDir, root, (rel) => {
152
+ if (isTestPath(rel)) pushFile(rel, true);
153
+ });
106
154
  }
107
155
 
108
- if (useCustomGlobs) {
109
- // Walk project roots and keep files matching custom globs.
110
- for (const dir of ['.', 'tests', 'test', 'src', '__tests__', 'spec']) {
111
- const absDir = path.join(root, dir === '.' ? '' : dir);
112
- if (!fs.existsSync(absDir)) continue;
113
- walkTestFiles(absDir, root, (rel) => {
114
- if (isTestPath(rel)) pushFile(rel, true);
115
- });
116
- }
117
- } else {
118
- // Walk common test roots when facts only cover production include globs.
119
- for (const dir of ['tests', 'test', 'src', '__tests__']) {
120
- const absDir = path.join(root, dir);
121
- if (!fs.existsSync(absDir)) continue;
122
- walkTestFiles(absDir, root, (rel) => {
123
- if (isTestPath(rel)) pushFile(rel, true);
124
- });
125
- }
156
+ for (const file of facts?.files ?? []) {
157
+ if (file?.path) pushFile(file.path);
126
158
  }
127
159
 
128
160
  const testGlobsMissing = testFiles.length === 0;
129
- return { fileContents, testFiles, testGlobsMissing };
161
+ return {
162
+ fileContents,
163
+ testFiles,
164
+ testGlobsMissing,
165
+ coverageBudgetExhausted: seen.size >= MAX_COVERAGE_FILES,
166
+ };
130
167
  }
131
168
 
132
169
  /**
@@ -38,6 +38,7 @@ export function evaluateInvariantCoverage(input) {
38
38
  }
39
39
  const testFiles = input.testFiles ?? [];
40
40
  const testGlobsMissing = input.testGlobsMissing === true || testFiles.length === 0;
41
+ const coverageBudgetExhausted = input.coverageBudgetExhausted === true;
41
42
  const coverage = [];
42
43
  const violations = [];
43
44
  for (const inv of invariants) {
@@ -83,7 +84,9 @@ export function evaluateInvariantCoverage(input) {
83
84
  violations.push({
84
85
  ruleId: 'INVARIANT_UNCOVERED',
85
86
  message: partial
86
- ? `Invariant ${inv.id} coverage cannot be proven (test globs missing or empty); reporting partial, not covered (never-had-tests).`
87
+ ? coverageBudgetExhausted
88
+ ? `Invariant ${inv.id} coverage cannot be proven (coverage file budget exhausted); reporting partial, not covered.`
89
+ : `Invariant ${inv.id} coverage cannot be proven (test globs missing or empty); reporting partial, not covered (never-had-tests).`
87
90
  : kind === 'tests-disappeared'
88
91
  ? `Invariant ${inv.id} is not covered by a test title or declared symbol (tests-disappeared — suite exists).`
89
92
  : `Invariant ${inv.id} is not covered by a test title or declared symbol (never-had-tests).`,
@@ -3,7 +3,10 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { analyzePolicyDelta } from './analysis-engine.mjs';
5
5
  import { loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
6
- import { loadInvariantCoverageInputs } from './invariant-coverage-io.mjs';
6
+ import {
7
+ invariantIdsFromCatalog,
8
+ loadInvariantCoverageInputs,
9
+ } from './invariant-coverage-io.mjs';
7
10
  import { evaluateInvariantCoverage } from './invariant-coverage.mjs';
8
11
 
9
12
  function readJsonFile(filePath, label) {
@@ -175,12 +178,15 @@ export function analyzePolicyTransition({
175
178
 
176
179
  let candidateInvariantCoverage;
177
180
  if ((candidateArkRules?.invariants?.length ?? 0) > 0) {
178
- const coverageInputs = loadInvariantCoverageInputs(root, { files: [] });
181
+ const coverageInputs = loadInvariantCoverageInputs(root, { files: [] }, {
182
+ invariantIds: invariantIdsFromCatalog(candidateArkRules),
183
+ });
179
184
  const evaluated = evaluateInvariantCoverage({
180
185
  arkRules: candidateArkRules,
181
186
  fileContents: coverageInputs.fileContents,
182
187
  testFiles: coverageInputs.testFiles,
183
188
  testGlobsMissing: coverageInputs.testGlobsMissing,
189
+ coverageBudgetExhausted: coverageInputs.coverageBudgetExhausted === true,
184
190
  });
185
191
  candidateInvariantCoverage = evaluated.coverage;
186
192
  }
@@ -6,7 +6,10 @@
6
6
  */
7
7
  import { loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
8
8
  import { evaluateInvariantCoverage } from './invariant-coverage.mjs';
9
- import { loadInvariantCoverageInputs } from './invariant-coverage-io.mjs';
9
+ import {
10
+ invariantIdsFromCatalog,
11
+ loadInvariantCoverageInputs,
12
+ } from './invariant-coverage-io.mjs';
10
13
  import {
11
14
  EXTRA_MERGE_TEETH_GOVERNED_FLOOR,
12
15
  composeMergePlanesHonesty,
@@ -89,13 +92,16 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
89
92
  const invariants = loaded.arkRules.invariants?.length ?? 0;
90
93
  const coverageInputs =
91
94
  invariants > 0
92
- ? loadInvariantCoverageInputs(root, facts ?? { files: [] })
95
+ ? loadInvariantCoverageInputs(root, facts ?? { files: [] }, {
96
+ invariantIds: invariantIdsFromCatalog(loaded.arkRules),
97
+ })
93
98
  : { fileContents: {}, testFiles: [], testGlobsMissing: false };
94
99
  const coverage = evaluateInvariantCoverage({
95
100
  arkRules: loaded.arkRules,
96
101
  fileContents: coverageInputs.fileContents,
97
102
  testFiles: coverageInputs.testFiles,
98
103
  testGlobsMissing: coverageInputs.testGlobsMissing,
104
+ coverageBudgetExhausted: coverageInputs.coverageBudgetExhausted === true,
99
105
  });
100
106
  const covById = new Map(
101
107
  (coverage.coverage ?? []).map((row) => [row.invariantId, row])
@@ -409,7 +409,7 @@ declare const ARK_ANALYSIS_RESULT_SCHEMA: {
409
409
  };
410
410
 
411
411
  /** ArkGate library version — single source of truth. */
412
- declare const version = "4.8.0";
412
+ declare const version = "4.8.2";
413
413
 
414
414
  /**
415
415
  * AI Code Gate (basic).
@@ -1691,6 +1691,11 @@ type EvaluateInvariantCoverageInput = {
1691
1691
  testFiles?: readonly string[];
1692
1692
  /** When true, missing test files make coverage partial (never green covered). */
1693
1693
  testGlobsMissing?: boolean;
1694
+ /**
1695
+ * Tooling hit MAX_COVERAGE_FILES. Partial must not claim the repo never had tests —
1696
+ * the suite may exist outside the scan budget.
1697
+ */
1698
+ coverageBudgetExhausted?: boolean;
1694
1699
  };
1695
1700
  declare function evaluateInvariantCoverage(input: EvaluateInvariantCoverageInput): {
1696
1701
  coverage: InvariantCoverageEvidence[];
@@ -1878,6 +1883,7 @@ type AnalyzeResolvedProjectInput = {
1878
1883
  fileContents: Readonly<Record<string, string>>;
1879
1884
  testFiles?: readonly string[];
1880
1885
  testGlobsMissing?: boolean;
1886
+ coverageBudgetExhausted?: boolean;
1881
1887
  };
1882
1888
  /**
1883
1889
  * AR07 — Tooling-supplied orchestration/thin-adapter heuristics per file.