arkgate 4.6.6 → 4.6.7

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 CHANGED
@@ -5,12 +5,40 @@ in the immutable pre-2.0 archive linked below.
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 4.6.7 — 2026-08-24
9
+
10
+ **Patch** over **4.6.6**. Production-hardening: CODEOWNERS, eval/pack honesty, CLI extracts,
11
+ spawn timeouts, and HTML list cap. **No required config migration.** Does not close Z09.
12
+
13
+ **Status: published** (on npm `latest`; see `docs/releases/4.6.7.md`).
14
+
15
+ ### Changed
16
+
17
+ - **CODEOWNERS:** `/src/`, `/bin/`, and `/schemas/` owned by `@pedroknigge`. GitHub still
18
+ needs `require_code_owner_reviews` (or an approving-review count) for that file to
19
+ enforce; the in-tree list is the product control.
20
+
21
+ - **Eval comparative fixture:** `saas-dashboard/without-ark` is a real Presentation→Domain
22
+ **value** import. Type-only was non-blocking and made the nightly oracle go false-green.
23
+
24
+ - **npm pack JSON:** `scripts/npm-pack-report.mjs` strips ANSI and parses JSON lines that
25
+ actually have `filename`. Empty `[]` / `{}` stay empty; missing filename still throws.
26
+
27
+ - **CLI extracts:** hook payload, package-manager helpers, and check args/config/watch live
28
+ in `bin/lib/`. Module-budget maxima were not raised.
29
+
30
+ - **git/gh timeouts:** `SPAWN_TIMEOUT_MS = 8000` on git/gh `spawnSync`. Timeout is
31
+ fail-closed (`status !== 0`).
32
+
33
+ - **HTML violation cap:** beginner and full showcase lists share a cap of 12 plus
34
+ `+N more (T total)`. KPI tiles still use the full array.
35
+
8
36
  ## 4.6.6 — 2026-08-22
9
37
 
10
38
  **Patch** over **4.6.5**. Phase AL corrective honesty plus a slimmer public docs surface.
11
39
  **No required config migration.** Does not close Z09. AL05 stays parked.
12
40
 
13
- **Status: current** (shipping as `arkgate@4.6.6`; see `docs/releases/4.6.6.md`).
41
+ **Status: published** (see `docs/releases/4.6.6.md`).
14
42
 
15
43
  ### Changed
16
44
 
@@ -49,7 +77,7 @@ in the immutable pre-2.0 archive linked below.
49
77
  **Patch** over **4.6.4**. Adoption, placement, doctor, upgrade, and write-path honesty for
50
78
  existing Next.js trees and multi-host teams. **No required config migration.**
51
79
 
52
- **Status: published** (on npm `latest` until 4.6.6 lands; see `docs/releases/4.6.5.md`).
80
+ **Status: published** (see `docs/releases/4.6.5.md`).
53
81
 
54
82
  ### Changed
55
83
 
package/README.md CHANGED
@@ -16,10 +16,10 @@ and makes sure a “green” check means something real.
16
16
 
17
17
  </div>
18
18
 
19
- > **ArkGate 4.6.6** is current (this train). **4.6.5** remains npm `latest` until publish.
19
+ > **ArkGate 4.6.7** is current on npm `latest`.
20
20
  > A tree is **adopted** only with a required GitHub status running `arkgate-check --strict-merge`,
21
21
  > or `.ark/adoption-stance.json` `stance: "advisory-only"`. Doctor is compact (`--doctor --all`
22
- > for Details). [4.6.6 notes](docs/releases/4.6.6.md) · [4.6.5](docs/releases/4.6.5.md) ·
22
+ > for Details). [4.6.7 notes](docs/releases/4.6.7.md) · [4.6.6](docs/releases/4.6.6.md) ·
23
23
  > [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md)
24
24
 
25
25
  ---
@@ -217,8 +217,9 @@ for real systems. Details: [docs/production-hardening.md](docs/production-harden
217
217
  | Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
218
218
  | Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
219
219
  | Security | [SECURITY.md](SECURITY.md) |
220
- | Current release (4.6.6) | [docs/releases/4.6.6.md](docs/releases/4.6.6.md) · [CHANGELOG](CHANGELOG.md) |
221
- | Prior published (4.6.5 on npm `latest` until 4.6.6 lands) | [docs/releases/4.6.5.md](docs/releases/4.6.5.md) |
220
+ | Current published (4.6.7 on npm `latest`) | [docs/releases/4.6.7.md](docs/releases/4.6.7.md) · [CHANGELOG](CHANGELOG.md) |
221
+ | Prior published (4.6.6) | [docs/releases/4.6.6.md](docs/releases/4.6.6.md) |
222
+ | Prior published (4.6.5) | [docs/releases/4.6.5.md](docs/releases/4.6.5.md) |
222
223
  | Prior published (4.6.3) | [docs/releases/4.6.3.md](docs/releases/4.6.3.md) |
223
224
  | Prior (4.6.2) | [docs/releases/4.6.2.md](docs/releases/4.6.2.md) |
224
225
  | Prior (4.6.1) | [docs/releases/4.6.1.md](docs/releases/4.6.1.md) |
@@ -7,7 +7,6 @@ import { fileURLToPath } from 'node:url';
7
7
  const __arkCheckCli = fileURLToPath(new URL('./ark-check.mjs', import.meta.url));
8
8
 
9
9
  import {
10
- DEFAULT_DOMAIN_FORBIDDEN_GLOBALS,
11
10
  DEFAULT_INTENT_PREFIXES,
12
11
  DEFAULT_LAYER_DIRECTORIES,
13
12
  DEFAULT_RULES,
@@ -80,7 +79,6 @@ import {
80
79
  CONCENTRATION_MIN_VIOLATIONS,
81
80
  } from './lib/violations.mjs';
82
81
  import {
83
- suggestLayerForDir,
84
82
  detectBestFitModel,
85
83
  dirSegmentsFromGlob,
86
84
  } from './lib/suggestions.mjs';
@@ -126,174 +124,9 @@ import {
126
124
  resolveEffectiveProjectRoot,
127
125
  } from './lib/project-root.mjs';
128
126
  import { demoteArkRuleTeethUnderClassificationFloor } from './lib/rules-under-contract.mjs';
129
-
130
- function resolveDesignDeltaBaseRef(root, explicit, env = process.env) {
131
- const flag = typeof explicit === 'string' ? explicit.trim() : '';
132
- if (flag) return flag;
133
- const envRef = normalizePolicyBaseRef(env.ARK_POLICY_BASE_REF);
134
- if (envRef) return envRef;
135
- const githubBase = typeof env.GITHUB_BASE_REF === 'string' ? env.GITHUB_BASE_REF.trim() : '';
136
- if (githubBase) return `origin/${githubBase}`;
137
- return discoverLocalBaseRef(root) || undefined;
138
- }
139
-
140
- function parseArgs(argv) {
141
- const args = {
142
- root: process.cwd(),
143
- config: 'ark.config.json',
144
- manifest: undefined,
145
- printConfig: undefined,
146
- tsconfig: undefined,
147
- json: false,
148
- strictConfig: false,
149
- strictMerge: false,
150
- requireGates: false,
151
- requireWriteHook: undefined,
152
- init: false,
153
- installAgentGates: false,
154
- compact: false,
155
- tools: undefined,
156
- force: false,
157
- skillsOnly: false,
158
- baseline: undefined,
159
- policyBase: undefined,
160
- policyBaseRef: undefined,
161
- policyAck: undefined, failOnNewSmells: false, baseRef: undefined,
162
- contractSession: false,
163
- contractDiff: false,
164
- changed: false,
165
- against: undefined,
166
- base: undefined,
167
- persona: undefined,
168
- author: undefined,
169
- failUngoverned: false,
170
- updateBaseline: false,
171
- noCache: false,
172
- resident: false,
173
- coverage: false,
174
- migrateCommands: false,
175
- doctor: false,
176
- plan: false,
177
- recommend: false,
178
- writePlan: false,
179
- listPolicyPacks: false,
180
- applyPolicyPack: undefined,
181
- watch: false,
182
- beginner: false,
183
- openReport: false,
184
- noOpenReport: false,
185
- version: false,
186
- help: false,
187
- all: false,
188
- followConfigRoot: false,
189
- };
190
- const requireValue = (flag, index) => {
191
- const value = argv[index + 1];
192
- if (value === undefined || value.startsWith('-')) {
193
- throw new Error(`Missing value for ${flag}. Run arkgate-check --help for usage.`);
194
- }
195
- return value;
196
- };
197
- for (let i = 2; i < argv.length; i += 1) {
198
- const arg = argv[i];
199
- if (arg === '--json') args.json = true;
200
- else if (arg === '--strict' || arg === '--strict-merge') {
201
- args.strictConfig = true;
202
- args.requireGates = true;
203
- args.strictMerge = true;
204
- }
205
- else if (arg === '--strict-config') args.strictConfig = true;
206
- else if (arg === '--require-gates') {
207
- args.requireGates = true;
208
- args.strictConfig = true;
209
- }
210
- else if (arg === '--require-write-hook') {
211
- args.requireWriteHook = requireValue(arg, i++).trim().toLowerCase();
212
- }
213
- else if (arg === '--init') args.init = true;
214
- else if (arg === '--preset') args.preset = requireValue(arg, i++);
215
- else if (arg === '--install-agent-gates') args.installAgentGates = true;
216
- else if (arg === '--compact') args.compact = true;
217
- else if (arg === '--tools') {
218
- // Consume the next arg only when it isn't another flag (same rule as --baseline),
219
- // so `--tools --force` can't silently eat --force as a "tool name".
220
- const next = argv[i + 1];
221
- if (next !== undefined && !next.startsWith('-')) {
222
- i += 1;
223
- args.tools = next
224
- .split(',')
225
- .map((tool) => tool.trim().toLowerCase())
226
- .filter(Boolean);
227
- } else {
228
- args.tools = []; // flag without a value — rejected in runInstallAgentGates
229
- }
230
- }
231
- else if (arg === '--force') args.force = true;
232
- else if (arg === '--follow-config-root') args.followConfigRoot = true;
233
- else if (arg === '--skills-only') args.skillsOnly = true;
234
- else if (arg === '--coverage') args.coverage = true;
235
- else if (arg === '--doctor') args.doctor = true;
236
- else if (arg === '--plan') args.plan = true;
237
- else if (arg === '--rules-inventory') args.rulesInventory = true;
238
- else if (arg === '--recommend') args.recommend = true;
239
- else if (arg === '--write-plan') args.writePlan = true;
240
- else if (arg === '--list-policy-packs') args.listPolicyPacks = true;
241
- else if (arg === '--apply-policy-pack') args.applyPolicyPack = requireValue(arg, i++);
242
- else if (arg === '--suggest-include') args.suggestInclude = true;
243
- else if (arg === '--adopt-contract') args.adoptContract = true;
244
- else if (arg === '--migrate-contract') args.migrateContract = true;
245
- else if (arg === '--ratchet-cores') args.ratchetCores = true;
246
- else if (arg === '--write') args.write = true;
247
- else if (arg === '--watch') args.watch = true;
248
- else if (arg === '--beginner') args.beginner = true;
249
- else if (arg === '--codex-home') args.codexHome = true;
250
- else if (arg === '--claude-home') args.claudeHome = true;
251
- else if (arg === '--grok-home') args.grokHome = true;
252
- else if (arg === '--agent-homes') {
253
- args.agentHomes = true;
254
- args.codexHome = true;
255
- args.claudeHome = true;
256
- args.grokHome = true;
257
- }
258
- else if (arg === '--migrate-commands') args.migrateCommands = true;
259
- else if (arg === '--no-cache') args.noCache = true;
260
- else if (arg === '--resident') args.resident = true;
261
- else if (arg === '--report') {
262
- const next = argv[i + 1];
263
- args.report = next && !next.startsWith('-') ? argv[++i] : 'ark-report.html';
264
- }
265
- else if (arg === '--reset-origin') args.resetOrigin = true;
266
- else if (arg === '--no-archive') args.noArchive = true;
267
- else if (arg === '--open') args.openReport = true;
268
- else if (arg === '--no-open') args.noOpenReport = true;
269
- else if (arg === '--baseline' || arg === '--update-baseline') {
270
- if (arg === '--update-baseline') args.updateBaseline = true;
271
- // optional path value: consume the next arg only when it isn't another flag
272
- const next = argv[i + 1];
273
- args.baseline = next && !next.startsWith('-') ? argv[++i] : '.ark-baseline.json';
274
- }
275
- else if (arg === '--policy-base') args.policyBase = requireValue(arg, i++);
276
- else if (arg === '--policy-base-ref') args.policyBaseRef = requireValue(arg, i++);
277
- else if (arg === '--policy-ack') args.policyAck = requireValue(arg, i++); else if (arg === '--fail-on-new-smells') args.failOnNewSmells = true; else if (arg === '--base-ref') args.baseRef = requireValue(arg, i++);
278
- else if (arg === '--contract-session') args.contractSession = true;
279
- else if (arg === '--contract-diff') args.contractDiff = true;
280
- else if (arg === '--changed') args.changed = true;
281
- else if (arg === '--against') args.against = requireValue(arg, i++);
282
- else if (arg === '--base') args.base = requireValue(arg, i++);
283
- else if (arg === '--persona') args.persona = requireValue(arg, i++);
284
- else if (arg === '--author') args.author = requireValue(arg, i++);
285
- else if (arg === '--root') args.root = path.resolve(requireValue(arg, i++));
286
- else if (arg === '--config') args.config = requireValue(arg, i++);
287
- else if (arg === '--manifest') args.manifest = requireValue(arg, i++);
288
- else if (arg === '--print-config') args.printConfig = requireValue(arg, i++);
289
- else if (arg === '--tsconfig') args.tsconfig = requireValue(arg, i++);
290
- else if (arg === '--help' || arg === '-h') args.help = true;
291
- else if (arg === '--all') args.all = true;
292
- else if (arg === '--version' || arg === '-V') args.version = true;
293
- else throw new Error(`Unknown argument: ${arg}. Run arkgate-check --help for usage.`);
294
- }
295
- return args;
296
- }
127
+ import { parseArgs, resolveDesignDeltaBaseRef } from './lib/check-args.mjs';
128
+ import { detectConfig, proposeForUncovered } from './lib/check-config-detect.mjs';
129
+ import { runWatchMode } from './lib/check-watch.mjs';
297
130
 
298
131
  /** Path shown to humans: project-relative when inside root, absolute otherwise (no `../../..`). */
299
132
  function displayPathFromRoot(root, absPath) {
@@ -317,101 +150,6 @@ function readConfig(root, configPath) {
317
150
  return parseArkConfigJson(fs.readFileSync(fullPath, 'utf8'), fullPath).config;
318
151
  }
319
152
 
320
- /**
321
- * Infer an ark.config.json from the directories that actually exist in the project,
322
- * using the same layer→directory conventions as the eleven-layer template. A directory
323
- * only counts when it contains at least one source file, so an empty scaffold dir can't
324
- * produce a layer whose pattern matches nothing (which --strict-config would fail).
325
- */
326
- function detectConfig(root) {
327
- const srcDir = fs.existsSync(path.join(root, 'src')) ? 'src' : '.';
328
- const layers = [];
329
-
330
- for (const entry of DEFAULT_INTENT_PREFIXES) {
331
- const directories = (DEFAULT_LAYER_DIRECTORIES[entry.layer] ?? []).filter(
332
- (directory) => walk(path.join(root, srcDir, directory), [], { root }).length > 0
333
- );
334
- if (directories.length === 0) continue;
335
- layers.push({
336
- name: entry.layer,
337
- patterns: directories.map((directory) => `${normalize(path.join(srcDir, directory))}/**`),
338
- intentPrefixes: entry.prefixes,
339
- ...(entry.layer === 'DomainModel'
340
- ? { forbiddenGlobals: DEFAULT_DOMAIN_FORBIDDEN_GLOBALS }
341
- : {}),
342
- });
343
- }
344
-
345
- const names = new Set(layers.map((layer) => layer.name));
346
- const rules = DEFAULT_RULES.filter((rule) => names.has(rule.from) && names.has(rule.to));
347
-
348
- return { srcDir, config: { include: [srcDir], layers, rules } };
349
- }
350
-
351
- /** Top-level directories under srcDir not covered by any detected layer pattern. */
352
- function uncoveredDirectories(root, srcDir, layers) {
353
- const base = path.join(root, srcDir);
354
- if (!fs.existsSync(base)) return [];
355
- return fs
356
- .readdirSync(base, { withFileTypes: true })
357
- .filter(
358
- (entry) =>
359
- entry.isDirectory() &&
360
- entry.name !== 'node_modules' &&
361
- entry.name !== 'dist' &&
362
- !entry.name.startsWith('.')
363
- )
364
- .map((entry) => entry.name)
365
- .filter((name) => {
366
- const prefix = `${normalize(path.join(srcDir, name))}/`;
367
- return !layers.some((layer) =>
368
- layer.patterns.some((pattern) => pattern.startsWith(prefix))
369
- );
370
- });
371
- }
372
-
373
- // detectWorkspaces: shared implementation in ark-shared.mjs (npm/pnpm/rush/lerna +
374
- // conventional multi-package roots).
375
-
376
- // Deny every "upward" edge for an ordered layer list (index 0 = outermost/top,
377
- // which may import everything below it). Inner/lower layers must not import outer
378
- // ones — the shared shape behind linear layered and feature-sliced layouts.
379
-
380
- function proposeForUncovered(root, srcDir, layers) {
381
- const proposals = [];
382
- for (const top of uncoveredDirectories(root, srcDir, layers)) {
383
- const direct = suggestLayerForDir(top);
384
- if (direct) {
385
- proposals.push({ dir: `${srcDir}/${top}`, ...direct });
386
- continue;
387
- }
388
- let children = [];
389
- try {
390
- children = fs
391
- .readdirSync(path.join(root, srcDir, top), { withFileTypes: true })
392
- .filter((e) => e.isDirectory() && e.name !== 'node_modules' && !e.name.startsWith('.'))
393
- .map((e) => e.name);
394
- } catch {
395
- /* not a readable directory — treat as unrecognized below */
396
- }
397
- if (children.length > 0) {
398
- // Descend: propose per child so a mixed `lib/` yields lib/repositories → Persistence
399
- // AND flags lib/db as unrecognized, instead of dropping the parts Ark can't place.
400
- for (const child of children) {
401
- const hit = suggestLayerForDir(child);
402
- proposals.push(
403
- hit
404
- ? { dir: `${srcDir}/${top}/${child}`, ...hit }
405
- : { dir: `${srcDir}/${top}/${child}`, unrecognized: true }
406
- );
407
- }
408
- } else {
409
- proposals.push({ dir: `${srcDir}/${top}`, unrecognized: true });
410
- }
411
- }
412
- return proposals;
413
- }
414
-
415
153
  function printInitNextSteps(root) {
416
154
  console.log('');
417
155
  console.log('Next steps:');
@@ -1964,87 +1702,17 @@ async function main() {
1964
1702
  }
1965
1703
 
1966
1704
  if (args.watch) {
1967
- await runWatchMode(args);
1705
+ await runWatchMode(args, {
1706
+ cliPath: __arkCheckCli,
1707
+ loadConfig: readConfig,
1708
+ dim: color.dim,
1709
+ });
1968
1710
  return;
1969
1711
  }
1970
1712
 
1971
1713
  process.exitCode = designCheck.exitCode(observedOk && (!args.strictMerge || analysisComplete) ? 0 : 1);
1972
1714
  }
1973
1715
 
1974
- async function runWatchMode(args) {
1975
- const argv = process.argv.slice(2).filter((token) => token !== '--watch');
1976
- let debounce;
1977
- const rerun = () => {
1978
- clearTimeout(debounce);
1979
- debounce = setTimeout(() => {
1980
- const result = spawnSync(process.execPath, [__arkCheckCli, ...argv], {
1981
- cwd: args.root,
1982
- stdio: 'inherit',
1983
- env: process.env,
1984
- });
1985
- process.exitCode = result.status ?? 1;
1986
- }, 300);
1987
- };
1988
-
1989
- let config;
1990
- try {
1991
- config = readConfig(args.root, args.config);
1992
- } catch (error) {
1993
- console.error(error instanceof Error ? error.message : String(error));
1994
- process.exitCode = 2;
1995
- return;
1996
- }
1997
-
1998
- for (const entry of config.include ?? []) {
1999
- const target = path.join(args.root, entry);
2000
- if (!fs.existsSync(target)) continue;
2001
- try {
2002
- const watcher = fs.watch(target, { recursive: true }, rerun);
2003
- watcher.on('error', () => {
2004
- watcher.close();
2005
- watchByPolling(target, rerun);
2006
- });
2007
- } catch {
2008
- watchByPolling(target, rerun);
2009
- }
2010
- }
2011
-
2012
- console.log(color.dim('Watching governed paths for changes… (Ctrl+C to stop)'));
2013
- await new Promise(() => {});
2014
- }
2015
-
2016
- function watchByPolling(target, onChange) {
2017
- let previous = watchFingerprint(target);
2018
- setInterval(() => {
2019
- const current = watchFingerprint(target);
2020
- if (current === previous) return;
2021
- previous = current;
2022
- onChange();
2023
- }, 250);
2024
- }
2025
-
2026
- function watchFingerprint(target) {
2027
- const pending = [target];
2028
- const entries = [];
2029
- while (pending.length > 0) {
2030
- const current = pending.pop();
2031
- let stat;
2032
- try {
2033
- stat = fs.statSync(current);
2034
- } catch {
2035
- continue;
2036
- }
2037
- entries.push(`${current}:${stat.mtimeMs}:${stat.size}`);
2038
- if (!stat.isDirectory()) continue;
2039
- try {
2040
- for (const name of fs.readdirSync(current)) pending.push(path.join(current, name));
2041
- } catch {
2042
- // A concurrent delete is represented by the next fingerprint.
2043
- }
2044
- }
2045
- return entries.sort().join('|');
2046
- }
2047
-
2048
1716
  main().catch((error) => {
2049
1717
  console.error(
2050
1718
  process.env.ARK_DEBUG_STACK === '1' && error instanceof Error