arkgate 4.8.22 → 4.8.24

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 (136) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +7 -5
  3. package/bin/ark-check-runtime.mjs +27 -78
  4. package/bin/ark-dashboard.mjs +56 -53
  5. package/bin/ark-layer-match.mjs +1117 -38
  6. package/bin/ark-mcp-runtime.mjs +412 -138
  7. package/bin/ark-mcp.mjs +68 -4
  8. package/bin/ark-shared.mjs +39 -0
  9. package/bin/ark.mjs +65 -97
  10. package/bin/lib/adapter-contract-types.mjs +5 -0
  11. package/bin/lib/adapter-contract.mjs +15 -0
  12. package/bin/lib/agent-projection-command.mjs +5 -1
  13. package/bin/lib/analysis-engine.mjs +15 -11
  14. package/bin/lib/architecture-scan.mjs +50 -6
  15. package/bin/lib/ark-order-doctor.mjs +1 -1
  16. package/bin/lib/ark-order-facts.mjs +410 -15
  17. package/bin/lib/ark-order-invariants.mjs +51 -9
  18. package/bin/lib/ark-order-sensors.mjs +28 -101
  19. package/bin/lib/ark-order-types.mjs +2 -0
  20. package/bin/lib/ark-run-doctor.mjs +14 -0
  21. package/bin/lib/ark-run-facts.mjs +434 -11
  22. package/bin/lib/ark-run-sensors.mjs +5 -3
  23. package/bin/lib/arkgate-pins.mjs +91 -0
  24. package/bin/lib/arkrules-sensors.mjs +50 -249
  25. package/bin/lib/check-args.mjs +23 -1
  26. package/bin/lib/ci-and-commands.mjs +16 -6
  27. package/bin/lib/class-source-scan.mjs +479 -0
  28. package/bin/lib/codex-home.mjs +4 -2
  29. package/bin/lib/config-contract-slices.mjs +701 -0
  30. package/bin/lib/config-contract.mjs +5 -2
  31. package/bin/lib/config-warnings.mjs +32 -0
  32. package/bin/lib/dashboard-cli.mjs +62 -0
  33. package/bin/lib/design-delta.mjs +1 -1
  34. package/bin/lib/diagnostic-catalog.mjs +45 -6
  35. package/bin/lib/doctor-advisories.mjs +84 -1
  36. package/bin/lib/doctor-next-actions.mjs +17 -2
  37. package/bin/lib/doctor-plan.mjs +7 -9
  38. package/bin/lib/effective-contract-load.mjs +121 -59
  39. package/bin/lib/enforcement-honesty.mjs +22 -0
  40. package/bin/lib/enforcement-profiles.mjs +40 -4
  41. package/bin/lib/first-run-help.mjs +3 -2
  42. package/bin/lib/flat-parent-importers.mjs +137 -0
  43. package/bin/lib/flat-parent-pilot.mjs +192 -0
  44. package/bin/lib/gate-files.mjs +29 -2
  45. package/bin/lib/git-change-scope.mjs +109 -0
  46. package/bin/lib/github-enforcement.mjs +1 -1
  47. package/bin/lib/hook-templates.mjs +18 -16
  48. package/bin/lib/host-support-matrix.mjs +3 -1
  49. package/bin/lib/html-report-advisories.mjs +2 -0
  50. package/bin/lib/html-report-depth.mjs +3 -1
  51. package/bin/lib/import-resolve.mjs +13 -1
  52. package/bin/lib/install-migrate.mjs +50 -73
  53. package/bin/lib/invariant-coverage.mjs +59 -11
  54. package/bin/lib/mcp-adoption.mjs +9 -1
  55. package/bin/lib/mcp-hook-payload.mjs +401 -61
  56. package/bin/lib/package-manager.mjs +59 -1
  57. package/bin/lib/passthrough-commands.mjs +83 -0
  58. package/bin/lib/persistence-write-hint.mjs +81 -0
  59. package/bin/lib/physical-cohesion.mjs +16 -18
  60. package/bin/lib/pilot-loop.mjs +21 -8
  61. package/bin/lib/pin-dependent-gates.mjs +117 -0
  62. package/bin/lib/policy-delta-io.mjs +168 -10
  63. package/bin/lib/prepare-change.mjs +120 -6
  64. package/bin/lib/prepare-write.mjs +1 -0
  65. package/bin/lib/presets.mjs +3 -5
  66. package/bin/lib/remediation.mjs +23 -4
  67. package/bin/lib/resolved-candidate-facts.mjs +315 -55
  68. package/bin/lib/rules-inventory.mjs +22 -3
  69. package/bin/lib/rules-under-contract.mjs +179 -5
  70. package/bin/lib/scan-files.mjs +32 -0
  71. package/bin/lib/snippet-analysis.mjs +140 -6
  72. package/bin/lib/source-policy.mjs +34 -0
  73. package/bin/lib/stable-hash.mjs +90 -0
  74. package/bin/lib/start-install-recovery.mjs +71 -1
  75. package/bin/lib/start-preview.mjs +30 -0
  76. package/bin/lib/status-command.mjs +33 -4
  77. package/bin/lib/status-manifest.mjs +32 -4
  78. package/bin/lib/team-parliament-io.mjs +79 -75
  79. package/bin/lib/team-parliament.mjs +6 -1
  80. package/bin/lib/write-path-capabilities.mjs +4 -2
  81. package/dist/chunk-A5EIY3HI.cjs +5 -0
  82. package/dist/chunk-MBXCFLO4.js +16 -0
  83. package/dist/chunk-RNQHRDCQ.cjs +16 -0
  84. package/dist/chunk-SVSQVGFJ.js +5 -0
  85. package/dist/{configTypes-CTELo87-.d.ts → configTypes-BJbaeoj4.d.cts} +53 -4
  86. package/dist/configTypes-BJbaeoj4.d.ts +246 -0
  87. package/dist/eslint/index.cjs +17 -4
  88. package/dist/eslint/index.d.cts +359 -0
  89. package/dist/eslint/index.d.ts +178 -158
  90. package/dist/eslint/index.js +10 -4
  91. package/dist/index.cjs +38 -37
  92. package/dist/index.d.cts +5475 -0
  93. package/dist/index.d.ts +3408 -296
  94. package/dist/index.js +38 -37
  95. package/dist/inspectorListen-6DQVJ4DC.js +1 -0
  96. package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
  97. package/dist/nestjs/index.cjs +1 -18
  98. package/dist/nestjs/index.d.cts +22 -0
  99. package/dist/nestjs/index.d.ts +1 -3
  100. package/dist/nestjs/index.js +1 -18
  101. package/dist/order/index.cjs +1 -1
  102. package/dist/order/index.d.cts +155 -0
  103. package/dist/order/index.d.ts +15 -3
  104. package/dist/order/index.js +1 -1
  105. package/dist/runtime/index.cjs +13 -30
  106. package/dist/{diagnosticCatalog-WD2TPSwZ.d.ts → runtime/index.d.cts} +814 -290
  107. package/dist/runtime/index.d.ts +2678 -108
  108. package/dist/runtime/index.js +13 -30
  109. package/dist/{types-yDNEQ603.d.ts → types-DRinG-vT.d.cts} +576 -140
  110. package/dist/types-DRinG-vT.d.ts +1792 -0
  111. package/docs/README.md +3 -1
  112. package/docs/agent-guide.md +92 -15
  113. package/docs/ai-gates.md +160 -26
  114. package/docs/arkorder.md +32 -15
  115. package/docs/brownfield-adoption.md +5 -1
  116. package/docs/configuration.md +273 -12
  117. package/docs/develop.md +2 -1
  118. package/docs/diagnostics.md +122 -13
  119. package/docs/package-surface.md +31 -8
  120. package/docs/product-voice.md +1 -1
  121. package/package.json +59 -18
  122. package/schemas/ark.analysis-result.schema.json +15 -0
  123. package/schemas/ark.config.schema.json +163 -4
  124. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  125. package/schemas/ark.status-manifest.schema.json +22 -0
  126. package/server.json +3 -9
  127. package/templates/agent-skills/README.md +1 -1
  128. package/templates/agent-skills/ark-adopt/SKILL.md +4 -1
  129. package/templates/agent-skills/ark-order/SKILL.md +4 -2
  130. package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
  131. package/templates/arkrules/PersistenceAdapters.json +1 -1
  132. package/templates/arkrules/PresentationAdapters.json +1 -1
  133. package/templates/skills/ark-adopt.md +4 -1
  134. package/templates/skills/ark-order.md +4 -2
  135. package/templates/skills/ark-runtime.md +6 -1
  136. package/dist/types-fQ9-KrBj.d.ts +0 -183
@@ -8,6 +8,8 @@
8
8
  * Pure CLI helper (bin/lib/arkrules-sensors.mjs). Zero Node I/O.
9
9
  */
10
10
 
11
+ import { findClassDeclarations, scanClassMembers } from './class-source-scan.mjs';
12
+ import { sourceHasPersistenceWrite, sourceImportsPersistenceDriverText } from './persistence-write-hint.mjs';
11
13
  /** Keep in lockstep with arkRulesTypes.ARK_RULE_TIER2_SENSOR_IDS (self-contained for CLI gen). */
12
14
  const ARK_RULE_TIER2_SENSOR_IDS = ['no-anemic-model'];
13
15
  /**
@@ -30,7 +32,6 @@ export const DOMAIN_EVENTS_PUSH_RE = new RegExp(`\\bthis\\.${EVENTS_ARRAY_PROP}\
30
32
  const EVENTS_ARRAY_RESET_RE = new RegExp(`^this\\.${EVENTS_ARRAY_PROP}\\s*=\\s*\\[\\s*\\]`);
31
33
  const ANY_THIS_EMPTY_ARRAY_RE = /^this\.[A-Za-z_][A-Za-z0-9_]*\s*=\s*\[\s*\]/;
32
34
  const THIS_FIELD_ASSIGNMENT_RE = /\bthis\.[A-Za-z_][A-Za-z0-9_]*\s*=(?!=)/g;
33
- const SHAPE_TRUNCATED_UNTIL = 'truncatedUntil';
34
35
  export function expectedDomainInvariantWordsPhrase() {
35
36
  return `${DOMAIN_INVARIANT_WORDS.join(', ')}, or events-array .push(`;
36
37
  }
@@ -59,18 +60,10 @@ function methodAssignsThis(methodName, methodBody) {
59
60
  return false;
60
61
  }
61
62
  function attachShapeTruncation(shape, truncatedUntil) {
62
- if (truncatedUntil == null)
63
- return shape;
64
- Object.defineProperty(shape, SHAPE_TRUNCATED_UNTIL, {
65
- value: truncatedUntil,
66
- enumerable: false,
67
- configurable: true,
68
- });
69
- return shape;
63
+ return truncatedUntil == null ? shape : { ...shape, truncatedUntil };
70
64
  }
71
65
  function shapeTruncatedUntil(shape) {
72
- const value = Object.getOwnPropertyDescriptor(shape, SHAPE_TRUNCATED_UNTIL)?.value;
73
- return typeof value === 'number' ? value : undefined;
66
+ return typeof shape.truncatedUntil === 'number' ? shape.truncatedUntil : undefined;
74
67
  }
75
68
  function shapeTruncationSuffix(shape) {
76
69
  const until = shapeTruncatedUntil(shape);
@@ -188,9 +181,22 @@ function shapesForRule(rule, shapes, layerForFile) {
188
181
  return true;
189
182
  });
190
183
  }
184
+ /**
185
+ * A shape the scanner could not walk to its end (unbalanced header/body) is
186
+ * reported, never passed in silence: later members may be invisible.
187
+ */
188
+ function truncatedShapeViolation(rule, shape, what) {
189
+ const until = shapeTruncatedUntil(shape);
190
+ if (until == null)
191
+ return undefined;
192
+ return baseViolation(rule, shape.file, `Exported class ${shape.className} shape analysed until character ${until}; later ${what} may be invisible (sensor ${rule.sensor}).`);
193
+ }
191
194
  function evaluateAggregatePrivateState(rule, shapes, layerForFile) {
192
195
  const out = [];
193
196
  for (const shape of shapesForRule(rule, shapes, layerForFile)) {
197
+ const truncated = truncatedShapeViolation(rule, shape, 'members');
198
+ if (truncated)
199
+ out.push(truncated);
194
200
  // P1-L — require real mutability (setters or non-readonly public fields).
195
201
  // Readonly public props on intentional value/entity shapes are false-positive-prone.
196
202
  if (shape.hasPublicSetters || shape.hasPublicMutableFields) {
@@ -202,6 +208,9 @@ function evaluateAggregatePrivateState(rule, shapes, layerForFile) {
202
208
  function evaluateAlwaysValidFactory(rule, shapes, layerForFile) {
203
209
  const out = [];
204
210
  for (const shape of shapesForRule(rule, shapes, layerForFile)) {
211
+ const truncated = truncatedShapeViolation(rule, shape, 'constructors and factories');
212
+ if (truncated)
213
+ out.push(truncated);
205
214
  if (shape.hasPublicConstructor && !shape.hasStaticFactory) {
206
215
  // P1-L — prefer false negatives on intentional DDD / DI aggregates: a public
207
216
  // constructor alone is weak evidence. Only fire when mutable public surface
@@ -221,9 +230,9 @@ function evaluateDomainEventOnMutation(rule, shapes, layerForFile) {
221
230
  const expected = expectedDomainInvariantWordsPhrase();
222
231
  for (const shape of shapesForRule(rule, shapes, layerForFile)) {
223
232
  const truncation = shapeTruncationSuffix(shape);
224
- if (shapeTruncatedUntil(shape) != null) {
225
- out.push(baseViolation(rule, shape.file, `Exported class ${shape.className} shape analysed until character ${shapeTruncatedUntil(shape)}; later methods may be invisible (sensor domain-event-on-mutation).`));
226
- }
233
+ const truncated = truncatedShapeViolation(rule, shape, 'methods');
234
+ if (truncated)
235
+ out.push(truncated);
227
236
  for (const method of shape.mutatingMethods) {
228
237
  if (!method.referencesGuardOrPublish) {
229
238
  out.push(baseViolation(rule, shape.file, `Mutating method ${shape.className}.${method.name} does not reference ${expected} (sensor domain-event-on-mutation).${truncation}`));
@@ -459,24 +468,11 @@ export function collectEmptyInvariantCatalogFindings(input) {
459
468
  },
460
469
  ];
461
470
  }
462
- /** IO / ORM import evidence. postgres and drizzle-orm include package subpaths. Keep in lockstep with arkOrderFacts. */
463
- const IO_IMPORT_HINT_RE = /\bfrom\s+['"](?:@?prisma\/client|@supabase\/|drizzle-orm(?:\/[^'"]+)?|postgres(?:\/[^'"]+)?|typeorm|knex|mongodb|pg|mysql2|mongoose|better-sqlite3|ioredis|redis|kysely|sequelize)['"]|require\(\s*['"](?:@?prisma\/client|pg|postgres(?:\/[^'"]+)?|drizzle-orm(?:\/[^'"]+)?|knex|typeorm|mongoose)/;
464
- /**
465
- * Path-alias / local db module (`@/lib/db`) without resolving tsconfig.
466
- * Keep in lockstep with arkOrderFacts.
467
- */
468
- const IO_ALIAS_IMPORT_RE = /\bfrom\s+['"](?:@\/|~\/)?(?:[\w.-]+\/)*(?:db|database|prisma|drizzle)(?:\.[cm]?[jt]sx?)?['"]|require\(\s*['"](?:@\/|~\/)?(?:[\w.-]+\/)*(?:db|database|prisma|drizzle)/;
469
- /**
470
- * Write tokens that skip the aggregate when paired with a persistence driver import.
471
- * Callee must be db|tx|client|prisma|drizzle (PrismaClient included); not repo.update(.
472
- * Keep in lockstep with arkOrderFacts.
473
- */
474
- const PERSISTENCE_WRITE_HINT_RE = /\b(?:db|tx|client|prisma(?:Client)?|drizzle)\b(?:\s*\.\s*[A-Za-z_]\w*)*\s*\.\s*(?:insert(?:One|Many)?|update(?:One|Many)?|upsert|delete(?:One|Many)?|createMany|create|replaceOne|findOneAnd(?:Update|Delete|Replace))\s*\(|\bINSERT\s+INTO\b|\bUPDATE\s+[A-Za-z_][\w.]*\s+SET\b|\bDELETE\s+FROM\b/i;
475
471
  export function isPersistenceDriverLayer(layer) {
476
472
  return layer === 'PersistenceAdapters';
477
473
  }
478
474
  export function sourceImportsPersistenceDriver(content, resolvedImports) {
479
- if (IO_IMPORT_HINT_RE.test(content) || IO_ALIAS_IMPORT_RE.test(content))
475
+ if (sourceImportsPersistenceDriverText(content))
480
476
  return true;
481
477
  if (!resolvedImports)
482
478
  return false;
@@ -487,7 +483,7 @@ export function sourceImportsPersistenceDriver(content, resolvedImports) {
487
483
  if (!specifier)
488
484
  continue;
489
485
  const synthetic = `from '${specifier}'`;
490
- if (IO_IMPORT_HINT_RE.test(synthetic) || IO_ALIAS_IMPORT_RE.test(synthetic))
486
+ if (sourceImportsPersistenceDriverText(synthetic))
491
487
  return true;
492
488
  }
493
489
  return false;
@@ -506,7 +502,7 @@ export function deriveArkRuleFileHints(_file, content, resolvedImports) {
506
502
  if (!content)
507
503
  return null;
508
504
  const hasIo = sourceImportsPersistenceDriver(content, resolvedImports);
509
- const persistenceWrite = hasIo && PERSISTENCE_WRITE_HINT_RE.test(content);
505
+ const persistenceWrite = hasIo && sourceHasPersistenceWrite(content);
510
506
  // Orchestration/adapter heuristics need a longer window; writes still fire on short probes.
511
507
  if (content.length < 40) {
512
508
  return persistenceWrite ? { persistenceWrite: true } : null;
@@ -549,213 +545,18 @@ export function buildArkRuleFileHints(fileContents, resolvedImportsByFile) {
549
545
  }
550
546
  return out;
551
547
  }
552
- const MEMBER_MODIFIERS = new Set([
553
- 'public',
554
- 'private',
555
- 'protected',
556
- 'static',
557
- 'async',
558
- 'readonly',
559
- 'abstract',
560
- 'override',
561
- 'declare',
562
- 'get',
563
- 'set',
564
- ]);
565
548
  const CONTROL_FLOW_METHOD_NAMES = new Set(['if', 'match', 'when']);
566
- function skipStringOrComment(src, index) {
567
- const ch = src[index];
568
- if (ch === '/' && src[index + 1] === '/') {
569
- const nl = src.indexOf('\n', index);
570
- return nl === -1 ? src.length : nl;
571
- }
572
- if (ch === '/' && src[index + 1] === '*') {
573
- const end = src.indexOf('*/', index + 2);
574
- return end === -1 ? src.length : end + 2;
575
- }
576
- if (ch === "'" || ch === '"' || ch === '`') {
577
- let j = index + 1;
578
- while (j < src.length) {
579
- if (src[j] === '\\') {
580
- j += 2;
581
- continue;
582
- }
583
- if (src[j] === ch)
584
- return j + 1;
585
- j += 1;
586
- }
587
- return src.length;
588
- }
589
- return index;
590
- }
591
- function skipWsAndComments(src, index) {
592
- let i = index;
593
- while (i < src.length) {
594
- if (/\s/.test(src[i])) {
595
- i += 1;
596
- continue;
597
- }
598
- if (src[i] === '/' && (src[i + 1] === '/' || src[i + 1] === '*')) {
599
- i = skipStringOrComment(src, i);
600
- continue;
601
- }
602
- break;
603
- }
604
- return i;
605
- }
606
- function readIdent(src, index) {
607
- const ch = src[index];
608
- if (!ch || !/[A-Za-z_]/.test(ch))
609
- return null;
610
- let j = index + 1;
611
- while (j < src.length && /[A-Za-z0-9_]/.test(src[j]))
612
- j += 1;
613
- return { ident: src.slice(index, j), end: j };
614
- }
615
- function skipBalanced(src, openIndex, openCh, closeCh) {
616
- if (src[openIndex] !== openCh)
617
- return null;
618
- let depth = 1;
619
- let i = openIndex + 1;
620
- while (i < src.length && depth > 0) {
621
- const skipped = skipStringOrComment(src, i);
622
- if (skipped !== i) {
623
- i = skipped;
624
- continue;
625
- }
626
- const ch = src[i];
627
- if (ch === openCh)
628
- depth += 1;
629
- else if (ch === closeCh)
630
- depth -= 1;
631
- i += 1;
632
- }
633
- return depth === 0 ? i : null;
634
- }
635
- function scanClassMembers(body) {
636
- const members = [];
637
- let i = 0;
638
- let truncatedAt;
639
- while (i < body.length) {
640
- i = skipWsAndComments(body, i);
641
- if (i >= body.length)
642
- break;
643
- if (body[i] === ';') {
644
- i += 1;
645
- continue;
646
- }
647
- const modifiers = [];
648
- let cursor = i;
649
- while (true) {
650
- const tok = readIdent(body, cursor);
651
- if (!tok || !MEMBER_MODIFIERS.has(tok.ident))
652
- break;
653
- modifiers.push(tok.ident);
654
- cursor = skipWsAndComments(body, tok.end);
655
- }
656
- const nameTok = readIdent(body, cursor);
657
- if (!nameTok) {
658
- i += 1;
659
- continue;
660
- }
661
- cursor = skipWsAndComments(body, nameTok.end);
662
- if (body[cursor] === '<') {
663
- const afterGeneric = skipBalanced(body, cursor, '<', '>');
664
- if (afterGeneric == null) {
665
- truncatedAt = body.length;
666
- break;
667
- }
668
- cursor = skipWsAndComments(body, afterGeneric);
669
- }
670
- if (body[cursor] === '(') {
671
- const afterParen = skipBalanced(body, cursor, '(', ')');
672
- if (afterParen == null) {
673
- truncatedAt = body.length;
674
- break;
675
- }
676
- cursor = skipWsAndComments(body, afterParen);
677
- if (body[cursor] === ':') {
678
- cursor += 1;
679
- while (cursor < body.length && body[cursor] !== '{' && body[cursor] !== ';') {
680
- const skipped = skipStringOrComment(body, cursor);
681
- if (skipped !== cursor) {
682
- cursor = skipped;
683
- continue;
684
- }
685
- cursor += 1;
686
- }
687
- }
688
- if (body[cursor] === '{') {
689
- const afterBrace = skipBalanced(body, cursor, '{', '}');
690
- if (afterBrace == null) {
691
- truncatedAt = body.length;
692
- break;
693
- }
694
- members.push({
695
- name: nameTok.ident,
696
- modifiers,
697
- kind: 'method',
698
- body: body.slice(cursor + 1, afterBrace - 1),
699
- });
700
- i = afterBrace;
701
- continue;
702
- }
703
- if (body[cursor] === ';') {
704
- i = cursor + 1;
705
- continue;
706
- }
707
- i = cursor + 1;
708
- continue;
709
- }
710
- let depthBrace = 0;
711
- let depthParen = 0;
712
- let depthBracket = 0;
713
- while (cursor < body.length) {
714
- const skipped = skipStringOrComment(body, cursor);
715
- if (skipped !== cursor) {
716
- cursor = skipped;
717
- continue;
718
- }
719
- const ch = body[cursor];
720
- if (ch === '{')
721
- depthBrace += 1;
722
- else if (ch === '}') {
723
- if (depthBrace === 0)
724
- break;
725
- depthBrace -= 1;
726
- }
727
- else if (ch === '(')
728
- depthParen += 1;
729
- else if (ch === ')')
730
- depthParen -= 1;
731
- else if (ch === '[')
732
- depthBracket += 1;
733
- else if (ch === ']')
734
- depthBracket -= 1;
735
- else if (ch === ';' &&
736
- depthBrace === 0 &&
737
- depthParen === 0 &&
738
- depthBracket === 0) {
739
- cursor += 1;
740
- break;
741
- }
742
- cursor += 1;
743
- }
744
- members.push({
745
- name: nameTok.ident,
746
- modifiers,
747
- kind: 'field',
748
- body: '',
749
- });
750
- i = cursor;
751
- }
752
- return { members, truncatedAt };
753
- }
754
549
  /**
755
550
  * Lightweight class-shape extraction from TypeScript source text (no compiler).
756
551
  * Conservative: prefers false negatives over false positives for mutability.
757
552
  * Tooling may replace with TypeScript-API facts; sensors consume the same shape.
758
553
  *
554
+ * The header and body are walked by the shared tokenizer (classSourceScan):
555
+ * type parameters (`class Box<T>`), generic `extends` / `implements` clauses, and
556
+ * braces inside strings, comments, or template literals are handled. A class whose
557
+ * header or body cannot be walked is still reported, with a truncation mark, so
558
+ * enforced sensors say so instead of passing in silence.
559
+ *
759
560
  * Limitation (AR05/AR06): only `export class` / `export abstract class` forms.
760
561
  * `export default class`, re-exported classes, and non-exported aggregates are
761
562
  * invisible — enforced structure sensors stay silent (false negative). Silence
@@ -763,25 +564,25 @@ function scanClassMembers(body) {
763
564
  */
764
565
  export function extractClassShapesFromSource(file, content) {
765
566
  const shapes = [];
766
- // Match exported class declarations (simple cases; see limitation above).
767
- const classRe = /export\s+(?:abstract\s+)?class\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:extends\s+[^{]+)?(?:implements\s+[^{]+)?\{/g;
768
- let match;
769
- while ((match = classRe.exec(content)) !== null) {
770
- const className = match[1];
771
- const start = match.index + match[0].length;
772
- // Brace match body
773
- let depth = 1;
774
- let i = start;
775
- while (i < content.length && depth > 0) {
776
- const ch = content[i];
777
- if (ch === '{')
778
- depth += 1;
779
- else if (ch === '}')
780
- depth -= 1;
781
- i += 1;
567
+ for (const decl of findClassDeclarations(content, { exportedOnly: true })) {
568
+ const className = decl.name;
569
+ if (decl.bodyStart == null) {
570
+ // Header seen, body not derivable: an unshaped class, never a silent pass.
571
+ shapes.push(attachShapeTruncation({
572
+ file,
573
+ className,
574
+ exported: true,
575
+ hasPublicMutableFields: false,
576
+ hasPublicSetters: false,
577
+ hasPublicConstructor: false,
578
+ hasStaticFactory: false,
579
+ mutatingMethods: [],
580
+ }, decl.start));
581
+ continue;
782
582
  }
783
- const body = content.slice(start, i - 1);
784
- const classUnclosed = depth > 0;
583
+ const start = decl.bodyStart;
584
+ const classUnclosed = decl.bodyEnd == null;
585
+ const body = content.slice(start, decl.bodyEnd ?? content.length);
785
586
  const scanned = scanClassMembers(body);
786
587
  const truncatedUntil = classUnclosed
787
588
  ? content.length
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * ark-check CLI flag parsing.
3
3
  */
4
+ import fs from 'node:fs';
4
5
  import path from 'node:path';
5
6
  import { discoverLocalBaseRef, normalizePolicyBaseRef } from './policy-delta-io.mjs';
6
7
 
@@ -23,7 +24,24 @@ export const LOCAL_STRICT_MERGE_MESSAGE =
23
24
  '--local cannot be combined with --strict-merge: CI stays the full fail-closed check. Use --local (or ARK_CHECK_LOCAL=1) only on the local / pre-push path.';
24
25
 
25
26
  export const LOCAL_TREE_MODE_MESSAGE =
26
- '--local cannot be combined with report modes that need the whole tree (--doctor, --coverage, --plan, --report, --promote). Use --local --base <ref> for the cheap local check.';
27
+ '--local cannot be combined with report modes that need the whole tree (--doctor, --coverage, --plan, --report, --promote) or with --update-baseline. Use --local --base <ref> for the cheap local check.';
28
+
29
+ export const UPDATE_BASELINE_SCOPE_MESSAGE =
30
+ '--update-baseline cannot be combined with a changed-files scope (--changed, --local, or a --persona that implies --changed): the baseline is a whole-tree record, and a changed-files scan would overwrite it with only that subset and delete every other frozen key. Drop the scope flag (ARK_CHECK_LOCAL=1 is ignored for a baseline update) and re-run.';
31
+
32
+ /**
33
+ * Baseline file that suppresses frozen findings for this run, or null.
34
+ * Explicit `--baseline` always wins. `--report` (without `--against`) applies the committed
35
+ * `.ark-baseline.json` by default, the same file `--doctor` reads, so report, `ark status`
36
+ * and doctor agree on what is active vs frozen.
37
+ */
38
+ export function effectiveBaselineName(args, root, exists = fs.existsSync) {
39
+ if (args?.baseline) return args.baseline;
40
+ // A merge verdict (--strict / --strict-merge / --contract-diff) never inherits the
41
+ // committed freeze implicitly: adding --report must not flip a fail-closed run to pass.
42
+ if (!args?.report || args.against || args.strictMerge || args.contractDiff) return null;
43
+ return exists(path.join(root, '.ark-baseline.json')) ? '.ark-baseline.json' : null;
44
+ }
27
45
 
28
46
  export function localCheckEnvelope(args, root) {
29
47
  if (!args?.local) return {};
@@ -48,6 +66,7 @@ export function applyLocalCheckMode(args, env = process.env) {
48
66
  args.plan && '--plan',
49
67
  args.report && '--report',
50
68
  args.promote && '--promote',
69
+ args.updateBaseline && '--update-baseline',
51
70
  ].filter(Boolean);
52
71
  if (args.strictMerge) {
53
72
  if (explicit) throw new Error(LOCAL_STRICT_MERGE_MESSAGE);
@@ -288,5 +307,8 @@ export function parseArgs(argv, env = process.env) {
288
307
  );
289
308
  }
290
309
  }
310
+ if (args.updateBaseline && (args.changed || args.local)) {
311
+ throw new Error(UPDATE_BASELINE_SCOPE_MESSAGE);
312
+ }
291
313
  return applyLocalCheckMode(args, env);
292
314
  }
@@ -21,6 +21,7 @@ import { renderHostSupportMatrixMarkdown } from './host-support-matrix.mjs';
21
21
  import { PREFERRED_MCP_BIN } from './hook-templates.mjs';
22
22
  import { hasCheckArchitectureScript, readPackageJson } from './gate-files.mjs';
23
23
  import { arkPackageVersion } from './skill-install.mjs';
24
+ import { arkgateIsProjectDependency, pinnedArkgateRunner } from './package-manager.mjs';
24
25
 
25
26
  // Field-install helpers re-exported for callers that import from this module.
26
27
  export {
@@ -43,6 +44,17 @@ export function checkArgsForRoot(root, { requireGates = false } = {}) {
43
44
 
44
45
 
45
46
  export function packageManager(root) {
47
+ const pm = localPackageManager(root);
48
+ if (arkgateIsProjectDependency(root)) return pm;
49
+ // arkgate is not pinned: the local runner would ask npm for a package named `ark-check`
50
+ // (404) or run a stale global, so name the exact arkgate that generated this workflow.
51
+ const run = `${pinnedArkgateRunner()} ark-check ${checkArgsForRoot(root, { requireGates: true })}`;
52
+ // No package.json: nothing to install, and setup-node's cache needs a lockfile.
53
+ if (!readPackageJson(root)) return { cache: null, setup: [], install: null, run };
54
+ return { ...pm, run };
55
+ }
56
+
57
+ function localPackageManager(root) {
46
58
  // CI always require-gates; baseline follows checkArgsForRoot.
47
59
  const checkArgs = checkArgsForRoot(root, { requireGates: true });
48
60
  // Same detection as every emitted command (execRunner): honors the packageManager field and
@@ -243,10 +255,11 @@ ${rows}`;
243
255
  * Load project layers for AGENTS generation. Returns null when config is absent/invalid
244
256
  * so callers fall back to the stock 11-layer table.
245
257
  * @param {string} root
258
+ * @param {string} [configPath] contract file (defaults to `<root>/ark.config.json`)
246
259
  */
247
- export function loadConfigLayersForAgents(root) {
260
+ export function loadConfigLayersForAgents(root, configPath = path.join(root, 'ark.config.json')) {
248
261
  try {
249
- const cfgPath = path.join(root, 'ark.config.json');
262
+ const cfgPath = configPath;
250
263
  if (!fs.existsSync(cfgPath)) return null;
251
264
  const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
252
265
  if (!Array.isArray(cfg?.layers) || cfg.layers.length === 0) return null;
@@ -638,10 +651,7 @@ ${setupSteps ? `${setupSteps}\n` : ''} - name: Setup Node
638
651
  uses: actions/setup-node@v4
639
652
  with:
640
653
  ${nodeSetup}
641
- cache: ${pm.cache}
642
- - name: Install dependencies
643
- run: ${pm.install}
644
- ${qualityBlock ? `${qualityBlock}\n` : ''} - name: Ark architecture check
654
+ ${pm.cache ? ` cache: ${pm.cache}\n` : ''}${pm.install ? ` - name: Install dependencies\n run: ${pm.install}\n` : ''}${qualityBlock ? `${qualityBlock}\n` : ''} - name: Ark architecture check
645
655
  env:
646
656
  ARK_POLICY_BASE_REF: \${{ github.event.pull_request.base.sha || github.event.before }}
647
657
  run: |