arkgate 4.0.1 → 4.1.1

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 (59) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +13 -13
  3. package/bin/ark-check-runtime.mjs +244 -25
  4. package/bin/ark-check.mjs +10 -1
  5. package/bin/ark-layer-match.mjs +80 -5
  6. package/bin/ark-shared.mjs +170 -9
  7. package/bin/ark.mjs +52 -5
  8. package/bin/lib/adapter-contract.mjs +7 -1
  9. package/bin/lib/agent-gates.mjs +2 -0
  10. package/bin/lib/analysis-engine.mjs +6 -6
  11. package/bin/lib/ark-gitignore.mjs +88 -0
  12. package/bin/lib/arkrules-sensors.mjs +63 -22
  13. package/bin/lib/ci-and-commands.mjs +165 -9
  14. package/bin/lib/core-ratchet.mjs +9 -4
  15. package/bin/lib/doctor-advisories.mjs +8 -1
  16. package/bin/lib/doctor-plan.mjs +287 -61
  17. package/bin/lib/enforcement-honesty.mjs +408 -27
  18. package/bin/lib/enforcement-state.mjs +1 -1
  19. package/bin/lib/field-install.mjs +35 -2
  20. package/bin/lib/github-enforcement.mjs +152 -4
  21. package/bin/lib/host-support-matrix.mjs +91 -17
  22. package/bin/lib/html-report-depth.mjs +178 -3
  23. package/bin/lib/html-report.mjs +19 -13
  24. package/bin/lib/install-migrate.mjs +109 -6
  25. package/bin/lib/managed-upgrade.mjs +99 -0
  26. package/bin/lib/presets.mjs +314 -46
  27. package/bin/lib/project-root.mjs +268 -0
  28. package/bin/lib/remediation.mjs +12 -11
  29. package/bin/lib/rules-inventory.mjs +71 -29
  30. package/bin/lib/rules-under-contract.mjs +134 -4
  31. package/bin/lib/start-preview.mjs +48 -14
  32. package/bin/lib/suggestions.mjs +118 -3
  33. package/bin/lib/unavailable-analysis.mjs +2 -0
  34. package/bin/lib/write-path-capabilities.mjs +38 -9
  35. package/bin/lib/write-path-detect.mjs +2 -2
  36. package/dist/eslint/index.cjs +2 -2
  37. package/dist/eslint/index.d.ts +27 -2
  38. package/dist/eslint/index.js +2 -2
  39. package/dist/index.cjs +16 -14
  40. package/dist/index.d.ts +3 -1
  41. package/dist/index.js +16 -14
  42. package/docs/README.md +5 -5
  43. package/docs/agent-guide.md +5 -3
  44. package/docs/ai-gates.md +45 -18
  45. package/docs/brownfield-adoption.md +36 -0
  46. package/docs/configuration.md +36 -0
  47. package/docs/develop.md +16 -6
  48. package/docs/package-surface.md +5 -3
  49. package/docs/product-voice.md +15 -2
  50. package/docs/typescript-support.md +9 -5
  51. package/docs/use.md +3 -1
  52. package/package.json +3 -1
  53. package/server.json +3 -3
  54. package/templates/architecture-playbook.json +3 -0
  55. package/templates/layers/shared-types.starter.json +29 -0
  56. package/templates/skills/ark-adopt.md +2 -0
  57. package/templates/skills/ark-explain.md +5 -0
  58. package/templates/skills/ark-explore.md +21 -1
  59. package/templates/skills/ark-fix.md +16 -5
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Pure / fail-closed: never invent hard write guarantees; never paint thin
5
5
  * coverage or a dirty freeze as "done." Advisory labels only.
6
+ * Never invents a numeric architecture score.
6
7
  */
7
8
  import {
8
9
  doctorWritePathHonestyMessage,
@@ -164,35 +165,369 @@ export function buildBaselineHonesty(input = {}) {
164
165
  /**
165
166
  * Write-path honesty for the active host (fail-closed).
166
167
  * Soft hosts never claim hard local write; hard hosts without proof stay unverified.
168
+ * Never hard:true without package install evidence (and pin when not self-host).
169
+ *
167
170
  * @param {string|null|undefined} activeHost
168
171
  * @param {boolean} hardWriteActive
172
+ * @param {{
173
+ * packageInstalled?: boolean,
174
+ * packagePinCode?: string | null,
175
+ * packagePinAbsent?: boolean,
176
+ * selfHost?: boolean,
177
+ * motherCli?: boolean,
178
+ * }} [extras]
169
179
  */
170
- export function buildWritePathHonesty(activeHost, hardWriteActive = false) {
180
+ export function buildWritePathHonesty(activeHost, hardWriteActive = false, extras = {}) {
171
181
  const host = typeof activeHost === 'string' ? activeHost.trim().toLowerCase() : '';
172
182
  const softWriteHost = SOFT_WRITE_HOSTS.has(host);
173
183
  const hardCapable = HARD_WRITE_HOSTS.has(host);
174
- const message = doctorWritePathHonestyMessage(host, hardWriteActive);
184
+ const packageInstalled = extras.packageInstalled !== false;
185
+ const selfHost = extras.selfHost === true;
186
+ const motherCli =
187
+ extras.motherCli === true ||
188
+ process.env.ARK_MOTHER_CLI === '1' ||
189
+ process.env.ARK_MOTHER_CLI === 'true';
190
+ const pinCode = typeof extras.packagePinCode === 'string' ? extras.packagePinCode : null;
191
+ const pinAbsent =
192
+ extras.packagePinAbsent === true ||
193
+ pinCode === 'PACKAGE_PIN_ABSENT';
194
+ // Self-host / mother library tree: pin-absent is expected (package IS arkgate).
195
+ const pinAbsentForUser = pinAbsent && !selfHost && !motherCli && pinCode !== 'PACKAGE_PIN_SELF_HOST';
196
+ // Hard write requires package on disk; pin-absent consumers never get hard:true.
197
+ const hardAllowed = packageInstalled && !pinAbsentForUser;
198
+ const effectiveHard =
199
+ Boolean(hardWriteActive) && hardCapable && !softWriteHost && hardAllowed;
200
+ const message = doctorWritePathHonestyMessage(host, effectiveHard);
175
201
 
176
- return {
202
+ /** @type {Record<string, unknown>} */
203
+ const out = {
177
204
  advisory: true,
178
205
  activeHost: host || null,
179
206
  softWriteHost,
180
207
  hardWriteSupported: hardCapable,
181
- hardWriteActive: Boolean(hardWriteActive) && hardCapable && !softWriteHost,
182
- hardWriteUnverified: hardCapable && !hardWriteActive,
183
- hardMergeBoundary: 'required-ci-status (arkgate-check --strict-merge)',
184
- message,
185
- // Explicit product rule for soft hosts.
186
- ...(softWriteHost
187
- ? {
188
- note: 'Local write is advisory / best-effort — not a hard PreToolUse boundary. Required CI status is the hard merge boundary.',
189
- }
190
- : {}),
208
+ hardWriteActive: effectiveHard,
209
+ hardWriteUnverified: hardCapable && !effectiveHard,
210
+ hardMergeBoundary:
211
+ 'required-github-status-context (CLI: arkgate-check --strict-merge / ark-check --strict-merge)',
212
+ packageInstalled,
213
+ packagePinAbsent: pinAbsentForUser,
214
+ ...(pinCode ? { packagePinCode: pinCode } : {}),
215
+ message:
216
+ pinAbsentForUser
217
+ ? `${message} Package pin absent (PACKAGE_PIN_ABSENT) — configured hooks ≠ installed enforcement until arkgate is pinned and in node_modules.`
218
+ : !packageInstalled && hardCapable
219
+ ? `${message} arkgate package not resolved from this project — hard write is not proven.`
220
+ : message,
221
+ };
222
+ if (softWriteHost) {
223
+ out.note =
224
+ 'Local write is advisory / best-effort — not a hard PreToolUse boundary. Required CI status is the hard merge boundary.';
225
+ }
226
+ if (pinAbsentForUser) {
227
+ out.pinNote =
228
+ 'No arkgate pin in package.json; CI/npx may not resolve this CLI version. Ladder/state hard stays false until pin + install.';
229
+ }
230
+ return out;
231
+ }
232
+
233
+ /**
234
+ * One coherent anti-false-green product surface (P0-B / FG01).
235
+ * finished is true only when residual honesty sensors are clear AND the graph
236
+ * + mode are green — never when blocking violations, adapt/suggest residual,
237
+ * missing baseline with debt, design residual, dual-truth, weak coverage,
238
+ * pin-absent, or residual pilots remain.
239
+ *
240
+ * activeBlockingViolations must be failsStrict !== false counts only
241
+ * (type-only placement debt must NOT force active-blocking-violations).
242
+ *
243
+ * Adapt/suggest policy (documented): unfinished when mode is adapt/suggest
244
+ * unless whole-tree green AND zero design smells AND zero blocking — contract
245
+ * and tree still disagree while operating outside enforce.
246
+ * Never invents a numeric architecture score.
247
+ *
248
+ * @param {{
249
+ * coverageHonesty?: ReturnType<typeof buildCoverageHonesty>,
250
+ * baselineHonesty?: ReturnType<typeof buildBaselineHonesty>,
251
+ * writePathHonesty?: ReturnType<typeof buildWritePathHonesty>,
252
+ * designWeak?: boolean,
253
+ * designWeakLabel?: string | null,
254
+ * designSmellCount?: number,
255
+ * designSmellsWithOpenEdges?: boolean,
256
+ * packageVersionTruth?: {
257
+ * dualTruth?: boolean,
258
+ * note?: string,
259
+ * code?: string,
260
+ * cliVersion?: string | null,
261
+ * } | null,
262
+ * residualPilots?: boolean,
263
+ * pilotTarget?: string | null,
264
+ * arkRulesMergeHonesty?: Record<string, unknown> | null,
265
+ * primaryNextAction?: string | null,
266
+ * operatingMode?: string | null,
267
+ * activeBlockingViolations?: number | null,
268
+ * }} input
269
+ */
270
+ export function buildProductHonesty(input = {}) {
271
+ const reasons = [];
272
+ const cov = input.coverageHonesty;
273
+ const base = input.baselineHonesty;
274
+ const write = input.writePathHonesty;
275
+ const designWeak = input.designWeak === true;
276
+ const dualTruth = input.packageVersionTruth?.dualTruth === true;
277
+ const pinCode = input.packageVersionTruth?.code || write?.packagePinCode || null;
278
+ const pinAbsent =
279
+ write?.packagePinAbsent === true ||
280
+ pinCode === 'PACKAGE_PIN_ABSENT';
281
+ const residualPilots = input.residualPilots === true;
282
+ const operatingMode =
283
+ typeof input.operatingMode === 'string' ? input.operatingMode.trim().toLowerCase() : null;
284
+ // Prefer explicit blocking count; never treat raw violation totals (incl. type-only) as blocking.
285
+ const activeBlocking = Number.isFinite(Number(input.activeBlockingViolations))
286
+ ? Math.max(0, Number(input.activeBlockingViolations))
287
+ : Number(base?.activeViolations) || 0;
288
+ const smellCount = Number(input.designSmellCount) || 0;
289
+ const designSmellsOpenEdges =
290
+ input.designSmellsWithOpenEdges === true || (smellCount > 0 && activeBlocking > 0);
291
+ const wholeTreeGovernedEarly = cov?.wholeTreeGoverned === true;
292
+
293
+ if (cov?.status === 'empty-scope' || cov?.worseThanNoGate) {
294
+ reasons.push({
295
+ id: 'coverage-weak-or-empty',
296
+ message: cov.message,
297
+ });
298
+ } else if (cov?.greenIsNotEnforcement) {
299
+ reasons.push({
300
+ id: 'coverage-partial',
301
+ message: cov.message,
302
+ });
303
+ }
304
+
305
+ // FG01 / P0B-FINISHED-WITH-OPEN-DEBT — red graph is never "finished".
306
+ if (activeBlocking > 0) {
307
+ reasons.push({
308
+ id: 'active-blocking-violations',
309
+ message: `${activeBlocking} active blocking violation(s) remain — not finished; green edges only after debt is cleared or honestly baselined.`,
310
+ });
311
+ }
312
+
313
+ if (base?.status === 'missing-with-debt') {
314
+ reasons.push({
315
+ id: 'baseline-missing-with-debt',
316
+ message:
317
+ base.message ||
318
+ 'No baseline while violations exist — freeze only real debt after the contract is honest.',
319
+ });
320
+ }
321
+
322
+ if (base?.dirtyBaselineRisk) {
323
+ reasons.push({
324
+ id: 'dirty-freeze',
325
+ message: base.message,
326
+ });
327
+ }
328
+
329
+ if (designWeak) {
330
+ reasons.push({
331
+ id: 'design-weak',
332
+ message:
333
+ input.designWeakLabel ||
334
+ 'ENFORCE · design-weak: edges may be clean, but design residual remains — not elegant, not finished.',
335
+ });
336
+ } else if (designSmellsOpenEdges) {
337
+ // DL-DESIGN-SMELLS-VS-WEAK — smells + open edges ⇒ unfinished (not "elegant true").
338
+ reasons.push({
339
+ id: 'design-smells-open-edges',
340
+ message:
341
+ 'Design smells present alongside open edge debt — not elegant, not finished. Fix edges first; Shape residual after green.',
342
+ });
343
+ }
344
+
345
+ if (dualTruth) {
346
+ reasons.push({
347
+ id: 'package-version-dual-truth',
348
+ message:
349
+ input.packageVersionTruth?.note ||
350
+ 'CLI version and package.json pin disagree — upgrade truth is dual until the pin catches up.',
351
+ });
352
+ } else if (pinAbsent) {
353
+ reasons.push({
354
+ id: 'package-pin-absent',
355
+ message:
356
+ input.packageVersionTruth?.note ||
357
+ write?.pinNote ||
358
+ 'No arkgate pin in package.json (PACKAGE_PIN_ABSENT) — configured gates ≠ installed enforcement until pin + install.',
359
+ });
360
+ }
361
+
362
+ if (residualPilots) {
363
+ reasons.push({
364
+ id: 'residual-pilot',
365
+ message: input.pilotTarget
366
+ ? `Residual pilot remains (${input.pilotTarget}) — one Shape/extraction card at a time; not whole-tree done.`
367
+ : 'Residual pilot pressure remains — one Shape/extraction card at a time; not whole-tree done.',
368
+ });
369
+ }
370
+
371
+ // EH05: soft-write-host is a permanent host posture residual — keep in evidence,
372
+ // do NOT alone force architecture "Not finished". Reclassified out of contract debt.
373
+ if (write?.softWriteHost) {
374
+ reasons.push({
375
+ id: 'soft-write-host',
376
+ bucket: 'environment',
377
+ message:
378
+ write.message ||
379
+ 'Local write is advisory; hard merge boundary = a required GitHub status context running arkgate-check --strict-merge (alias ark-check --strict-merge).',
380
+ });
381
+ }
382
+
383
+ // Mode adapt/suggest (FG-FINISHED-ADAPT-DEBT): prefer unfinished unless the tree is
384
+ // whole-tree green AND zero design smells AND zero blocking violations.
385
+ // Type-only placement debt alone must not keep adapt unfinished via active-blocking.
386
+ if (operatingMode === 'adapt' || operatingMode === 'suggest') {
387
+ const adaptClear =
388
+ wholeTreeGovernedEarly &&
389
+ activeBlocking === 0 &&
390
+ smellCount === 0 &&
391
+ !designWeak &&
392
+ !designSmellsOpenEdges;
393
+ if (!adaptClear) {
394
+ reasons.push({
395
+ id: operatingMode === 'adapt' ? 'mode-adapt-with-debt' : 'mode-suggest-with-debt',
396
+ message:
397
+ operatingMode === 'adapt'
398
+ ? 'Operating mode is ADAPT — not finished until whole-tree green, zero blocking, and zero design smells (contract and tree still disagree).'
399
+ : 'Operating mode is SUGGEST — not finished until whole-tree green, zero blocking, and zero design smells (contract is not yet the control plane).',
400
+ });
401
+ }
402
+ }
403
+
404
+ if (input.arkRulesMergeHonesty?.active === true && input.arkRulesMergeHonesty?.extraMergeTeeth === false) {
405
+ // Informational only when no enforced arkrule plane — does not alone make unfinished.
406
+ }
407
+
408
+ // EH05: environment residual deny-list (future reason ids stay architecture debt by default).
409
+ const ENVIRONMENT_REASON_IDS = new Set(['soft-write-host']);
410
+
411
+ const environmentResiduals = reasons.filter((r) => ENVIRONMENT_REASON_IDS.has(r.id));
412
+ const architectureReasons = reasons.filter((r) => !ENVIRONMENT_REASON_IDS.has(r.id));
413
+ // unfinished = any non-environment residual (deny-list env, not allowlist architecture)
414
+ const unfinished = architectureReasons.length > 0;
415
+ const wholeTreeGoverned = wholeTreeGovernedEarly;
416
+ const coverageIncomplete =
417
+ cov?.status === 'empty-scope' ||
418
+ cov?.worseThanNoGate === true ||
419
+ cov?.greenIsNotEnforcement === true ||
420
+ !wholeTreeGoverned;
421
+
422
+ const softWriteOnly =
423
+ !unfinished && environmentResiduals.some((r) => r.id === 'soft-write-host');
424
+ const hostLabel = (() => {
425
+ const h = typeof write?.activeHost === 'string' ? write.activeHost.trim().toLowerCase() : '';
426
+ if (h === 'codex') return 'Codex';
427
+ if (h === 'cursor') return 'Cursor';
428
+ if (h === 'opencode') return 'OpenCode';
429
+ if (h) return h;
430
+ return 'this host';
431
+ })();
432
+
433
+ const primary =
434
+ architectureReasons.find((r) => r.id === 'active-blocking-violations') ||
435
+ architectureReasons.find((r) => r.id === 'mode-adapt-with-debt') ||
436
+ architectureReasons.find((r) => r.id === 'mode-suggest-with-debt') ||
437
+ architectureReasons.find((r) => r.id === 'design-weak') ||
438
+ architectureReasons.find((r) => r.id === 'design-smells-open-edges') ||
439
+ architectureReasons.find((r) => r.id === 'coverage-weak-or-empty') ||
440
+ architectureReasons.find((r) => r.id === 'dirty-freeze') ||
441
+ architectureReasons.find((r) => r.id === 'package-version-dual-truth') ||
442
+ architectureReasons.find((r) => r.id === 'package-pin-absent') ||
443
+ architectureReasons.find((r) => r.id === 'baseline-missing-with-debt') ||
444
+ architectureReasons.find((r) => r.id === 'residual-pilot') ||
445
+ architectureReasons[0] ||
446
+ environmentResiduals[0];
447
+
448
+ let primaryMessage;
449
+ if (unfinished) {
450
+ primaryMessage =
451
+ primary?.message ||
452
+ 'Not finished: residual honesty signals remain (violations, mode, coverage, freeze, design, package pin, or pilots).';
453
+ } else if (softWriteOnly) {
454
+ primaryMessage = `${hostLabel} local writes stay advisory/bypassable; architecture contract on this slice is ready. Hard merge boundary is a required GitHub status context running arkgate-check --strict-merge (alias ark-check --strict-merge).`;
455
+ } else if (wholeTreeGoverned) {
456
+ primaryMessage =
457
+ 'No residual honesty blockers on this slice — still not a numeric architecture score; re-doctor after material change.';
458
+ } else {
459
+ primaryMessage =
460
+ 'No residual honesty blockers flagged — green is only as wide as the governed slice.';
461
+ }
462
+
463
+ // P0B-HEADLINE: dual-truth / pin-only unfinished must not claim "not whole-tree"
464
+ // when the governed tree is already 100%.
465
+ // EH05: soft-write alone → composite readiness headline, never global "Not finished".
466
+ let headline;
467
+ if (!unfinished && softWriteOnly) {
468
+ headline = wholeTreeGoverned
469
+ ? `Architecture contract ready; ${hostLabel} local writes are advisory`
470
+ : `Contract residual clear; ${hostLabel} local writes are advisory`;
471
+ } else if (!unfinished) {
472
+ headline = 'Honesty clear on residual signals';
473
+ } else if (coverageIncomplete) {
474
+ headline = 'Not finished / not whole-tree guarantee';
475
+ } else {
476
+ headline = 'Not finished';
477
+ }
478
+
479
+ // Prefer caller next action; dual-truth / pin-absent get install/pin path when empty.
480
+ // Soft-write-only must not leave a failure headline with null next action (EH05).
481
+ let primaryNextAction = input.primaryNextAction || null;
482
+ if (!primaryNextAction && dualTruth) {
483
+ const ver = input.packageVersionTruth?.cliVersion;
484
+ primaryNextAction = ver
485
+ ? `Bump package.json arkgate pin to ${ver} (or re-run install without --no-install)`
486
+ : 'Bump package.json arkgate pin to match this CLI (or re-run install without --no-install)';
487
+ } else if (!primaryNextAction && pinAbsent) {
488
+ primaryNextAction =
489
+ 'Add arkgate to package.json and install so CI/npx resolve this CLI (PACKAGE_PIN_ABSENT)';
490
+ } else if (!primaryNextAction && softWriteOnly) {
491
+ primaryNextAction =
492
+ 'Confirm the GitHub required status context name runs arkgate-check --strict-merge (or ark-check --strict-merge). Soft-write hosts stay advisory at local write; the required status is the hard merge boundary.';
493
+ }
494
+
495
+ const contractReadiness = unfinished ? 'not-ready' : wholeTreeGoverned ? 'ready' : 'partial';
496
+ const localWriteBoundary = write?.softWriteHost
497
+ ? 'advisory'
498
+ : write?.hardWriteActive
499
+ ? 'hard'
500
+ : write?.hardWriteSupported
501
+ ? 'unverified'
502
+ : 'unknown';
503
+
504
+ return {
505
+ finished: !unfinished && wholeTreeGoverned && !designWeak && activeBlocking === 0,
506
+ elegant: !designWeak && !base?.dirtyBaselineRisk && !designSmellsOpenEdges && activeBlocking === 0,
507
+ wholeTreeGuarantee:
508
+ wholeTreeGoverned &&
509
+ !designWeak &&
510
+ !base?.dirtyBaselineRisk &&
511
+ !cov?.greenIsNotEnforcement &&
512
+ activeBlocking === 0,
513
+ unfinished,
514
+ notAScore: true,
515
+ // Full evidence including soft-write-host (reclassified, not silenced)
516
+ reasonIds: reasons.map((r) => r.id),
517
+ reasons,
518
+ architectureReasonIds: architectureReasons.map((r) => r.id),
519
+ environmentResidualIds: environmentResiduals.map((r) => r.id),
520
+ environmentResiduals,
521
+ contractReadiness,
522
+ localWriteBoundary,
523
+ primaryMessage,
524
+ primaryNextAction,
525
+ headline,
191
526
  };
192
527
  }
193
528
 
194
529
  /**
195
- * One-shot doctor honesty bundle (coverage + baseline + write path).
530
+ * One-shot doctor honesty bundle (coverage + baseline + write path + product surface).
196
531
  * Keeps doctor-plan.mjs under its module budget.
197
532
  */
198
533
  export function computeDoctorEnforcementHonesty({
@@ -202,24 +537,70 @@ export function computeDoctorEnforcementHonesty({
202
537
  baselineExists,
203
538
  frozenKeys,
204
539
  activeViolations,
540
+ /** failsStrict !== false count only — type-only must not force active-blocking. */
541
+ activeBlockingViolations,
205
542
  suppressed,
206
543
  totalViolations,
207
544
  activeHost,
208
545
  hardWriteActive,
546
+ designWeak,
547
+ designWeakLabel,
548
+ designSmellCount,
549
+ designSmellsWithOpenEdges,
550
+ packageVersionTruth,
551
+ residualPilots,
552
+ pilotTarget,
553
+ arkRulesMergeHonesty,
554
+ primaryNextAction,
555
+ operatingMode,
556
+ packageInstalled,
557
+ selfHost,
558
+ motherCli,
209
559
  } = {}) {
560
+ const coverageHonesty = buildCoverageHonesty({
561
+ percent: governedPercent,
562
+ totalFiles,
563
+ emptyScope,
564
+ });
565
+ const baselineHonesty = buildBaselineHonesty({
566
+ exists: baselineExists,
567
+ frozenKeys,
568
+ activeViolations,
569
+ suppressed,
570
+ totalViolations,
571
+ });
572
+ const writePathHonesty = buildWritePathHonesty(activeHost, hardWriteActive, {
573
+ packageInstalled,
574
+ packagePinCode: packageVersionTruth?.code,
575
+ packagePinAbsent: packageVersionTruth?.code === 'PACKAGE_PIN_ABSENT',
576
+ selfHost,
577
+ motherCli,
578
+ });
579
+ // Prefer explicit blocking count; fall back to activeViolations only when callers
580
+ // already pass blocking-only totals (legacy tests). Type-only must not invent debt.
581
+ const blockingForHonesty = Number.isFinite(Number(activeBlockingViolations))
582
+ ? Math.max(0, Number(activeBlockingViolations))
583
+ : Number(activeViolations) || 0;
584
+ const productHonesty = buildProductHonesty({
585
+ coverageHonesty,
586
+ baselineHonesty,
587
+ writePathHonesty,
588
+ designWeak,
589
+ designWeakLabel,
590
+ designSmellCount,
591
+ designSmellsWithOpenEdges,
592
+ packageVersionTruth,
593
+ residualPilots,
594
+ pilotTarget,
595
+ arkRulesMergeHonesty,
596
+ primaryNextAction,
597
+ operatingMode,
598
+ activeBlockingViolations: blockingForHonesty,
599
+ });
210
600
  return {
211
- coverageHonesty: buildCoverageHonesty({
212
- percent: governedPercent,
213
- totalFiles,
214
- emptyScope,
215
- }),
216
- baselineHonesty: buildBaselineHonesty({
217
- exists: baselineExists,
218
- frozenKeys,
219
- activeViolations,
220
- suppressed,
221
- totalViolations,
222
- }),
223
- writePathHonesty: buildWritePathHonesty(activeHost, hardWriteActive),
601
+ coverageHonesty,
602
+ baselineHonesty,
603
+ writePathHonesty,
604
+ productHonesty,
224
605
  };
225
606
  }
@@ -1,2 +1,2 @@
1
1
  // Generated from enforcement-state.source.mjs — run npm run generate:packaged-tooling.
2
- import b from"node:fs";import{createRequire as q}from"node:module";import h from"node:path";const n="unverified";function S(a){const e=h.join(a,"package.json");try{if(JSON.parse(b.readFileSync(e,"utf8"))?.name==="arkgate"&&b.statSync(h.join(a,"bin","ark-check.mjs"),{throwIfNoEntry:!1})?.isFile())return{installed:!0,source:"package.json + bin/ark-check.mjs (self-host)"}}catch{}try{const r=q(e).resolve("arkgate/package.json"),o=h.dirname(r),t=JSON.parse(b.readFileSync(r,"utf8")),l=h.join(o,"bin","ark-check.mjs");if(t?.name==="arkgate"&&b.statSync(l,{throwIfNoEntry:!1})?.isFile())return{installed:!0,source:"arkgate/package.json via project resolver"}}catch{}return{installed:!1,source:"arkgate/package.json unresolved from project"}}function M(a){return a.length>0?a:["filesystem scan (no matching configuration)"]}function y({supported:a,configuredPaths:e,installed:r,active:o,runtimeObserved:t,operation:l,operationCoverage:d,bypassable:u,required:c,hard:i,sources:s}){const p=e.length>0,v=!!r.installed;return{supported:a,analyzed:!0,configured:p,installed:v,active:o,runtimeObserved:t,operation:l,operationCoverage:d,bypassable:u,required:c,hard:i,evidence:[...M(e).map(g=>({field:"configured",source:g,value:p})),{field:"installed",source:r.source,value:v},{field:"active",source:s.active,value:o},{field:"runtimeObserved",source:s.runtimeObserved,value:t},{field:"operationCoverage",source:s.operationCoverage,value:d},{field:"bypassable",source:s.bypassable,value:u},{field:"required",source:s.required,value:c},{field:"hard",source:s.hard,value:i}]}}function O(a,e){const r=S(a),o=!!e.support?.capabilities?.["hard-write"],t=!!e.support?.capabilities?.["advisory-write"],l=e.capabilityEvidence["hard-write"],d=e.capabilityEvidence["advisory-write"],u=e.capabilityEvidence["merge-gate"],c=e.enforcementLadder.localWrite,i=typeof c.operationCovered=="boolean",s=i?c.operationCovered:n,p=i&&s===!0,v=!!(o&&p&&c.hard===!0),g=i?p:o&&l.length>0&&r.installed?n:!1,k=t&&d.length>0&&r.installed?n:!1,f=!!(e.ci?.failClosed&&u.length>0),C=f&&r.installed?n:!1;return{schemaVersion:"1.1",activeHost:e.activeHost,localWrite:y({supported:o,configuredPaths:l,installed:r,active:g,runtimeObserved:i,operation:i?c.operation??null:null,operationCoverage:s,bypassable:v?!1:o&&!i?n:!0,required:n,hard:v,sources:{active:i?"observed PreToolUse attempt":"runtime observation unavailable",runtimeObserved:i?"fresh PreToolUse invocation":"runtime observation unavailable",operationCoverage:i?"active-host operation matcher":"operation not observed",bypassable:v?"observed hard write boundary":"host runtime bypass evidence unavailable",required:"local host policy unavailable",hard:v?"fresh covered active-host invocation":"hardness not proven for this invocation"}}),advisoryMcp:y({supported:t,configuredPaths:d,installed:r,active:k,runtimeObserved:!1,operation:null,operationCoverage:n,bypassable:!0,required:n,hard:!1,sources:{active:"MCP runtime observation unavailable",runtimeObserved:"doctor did not observe an MCP tool invocation",operationCoverage:"advisory MCP is caller-invoked",bypassable:"advisory MCP does not intercept every write",required:"local host policy unavailable",hard:"MCP presence is advisory and never proves a hard boundary"}}),ciMerge:y({supported:!0,configuredPaths:f?u:[],installed:r,active:C,runtimeObserved:!1,operation:"merge",operationCoverage:f?n:!1,bypassable:f?n:!0,required:n,hard:!1,sources:{active:"CI run and provider enforcement not observed",runtimeObserved:"provider evidence unavailable",operationCoverage:"required-status operation coverage unavailable",bypassable:"branch-protection evidence unavailable",required:"branch-protection evidence unavailable",hard:"merge hardness requires fresh provider evidence"}})}}function w(a,e,r,o){return{...a,...o,evidence:[...a.evidence.filter(t=>!e.includes(t.field)),...e.map(t=>({field:t,source:r,value:o[t]}))]}}function x(a,e){if(!e?.available)return a;const r=typeof e.arkCheckRequired=="boolean"?e.arkCheckRequired:n,o=!!(a.enforcementState.ciMerge.configured&&a.enforcementState.ciMerge.installed),t=r===!0?o:r===!1?!1:o?n:!1,l=t===!0?e.arkCheckSourceBound===!1?!0:n:r===!1?!0:o?n:!0,d=`GitHub branch protection (${e.repo??"repository"}:${e.branch??"default"})`,u=!0,c=r,i=t===!0&&l===!1&&c===!0,s=w(a.enforcementState.ciMerge,["active","runtimeObserved","operationCoverage","bypassable","required","hard"],d,{active:t,runtimeObserved:u,operationCoverage:c,bypassable:l,required:r,hard:i});return{...a,enforcementState:{...a.enforcementState,ciMerge:s},enforcementLadder:{...a.enforcementLadder,ciMerge:{...a.enforcementLadder.ciMerge,requiredStatus:r}}}}function m(a,e){const r=o=>o===!0?"yes":o===!1?"no":String(o);return`${a} \u2014 supported: ${r(e.supported)} \xB7 analyzed: ${r(e.analyzed)} \xB7 configured: ${r(e.configured)} \xB7 installed: ${r(e.installed)} \xB7 runtime observed: ${r(e.runtimeObserved)} \xB7 operation: ${e.operation??"none"} \xB7 operation covered: ${r(e.operationCoverage)} \xB7 active: ${r(e.active)} \xB7 bypassable: ${r(e.bypassable)} \xB7 required: ${r(e.required)} \xB7 hard: ${r(e.hard)}`}function P(a){const e=[{level:a.localWrite.active===!0?"ok":"warn",text:m("Local write",a.localWrite)},{level:"warn",text:m("Advisory MCP",a.advisoryMcp)},{level:a.ciMerge.required===!0?"ok":"warn",text:m("CI merge",a.ciMerge)}];return a.localWrite.active===n&&a.localWrite.hard===!1&&e.push({level:"bad",text:"RED FLAG: local hook assets exist, but this active-host operation was not observed at runtime; hard blocking is unverified."}),a.activeHost==="unknown"&&e.push({level:"warn",text:"Active host unknown for this invocation \u2014 enforcementState is session projection only. See writePath.inventory for on-disk host hooks; hard write is never claimed without runtime proof."}),e}export{O as buildEnforcementState,P as enforcementDoctorLines,x as withCiProviderEvidence};
2
+ import m from"node:fs";import{createRequire as q}from"node:module";import h from"node:path";const n="unverified";function S(a){const e=h.join(a,"package.json");try{if(JSON.parse(m.readFileSync(e,"utf8"))?.name==="arkgate"&&m.statSync(h.join(a,"bin","ark-check.mjs"),{throwIfNoEntry:!1})?.isFile())return{installed:!0,source:"package.json + bin/ark-check.mjs (self-host)",selfHost:!0}}catch{}try{const r=q(e).resolve("arkgate/package.json"),o=h.dirname(r),i=JSON.parse(m.readFileSync(r,"utf8")),s=h.join(o,"bin","ark-check.mjs");if(i?.name==="arkgate"&&m.statSync(s,{throwIfNoEntry:!1})?.isFile())return{installed:!0,source:"arkgate/package.json via project resolver",selfHost:!1}}catch{}return{installed:!1,source:"arkgate/package.json unresolved from project",selfHost:!1}}function M(a){return a.length>0?a:["filesystem scan (no matching configuration)"]}function g({supported:a,configuredPaths:e,installed:r,active:o,runtimeObserved:i,operation:s,operationCoverage:v,bypassable:u,required:c,hard:t,sources:l}){const p=e.length>0,d=!!r.installed;return{supported:a,analyzed:!0,configured:p,installed:d,active:o,runtimeObserved:i,operation:s,operationCoverage:v,bypassable:u,required:c,hard:t,evidence:[...M(e).map(f=>({field:"configured",source:f,value:p})),{field:"installed",source:r.source,value:d},{field:"active",source:l.active,value:o},{field:"runtimeObserved",source:l.runtimeObserved,value:i},{field:"operationCoverage",source:l.operationCoverage,value:v},{field:"bypassable",source:l.bypassable,value:u},{field:"required",source:l.required,value:c},{field:"hard",source:l.hard,value:t}]}}function O(a,e){const r=S(a),o=!!e.support?.capabilities?.["hard-write"],i=!!e.support?.capabilities?.["advisory-write"],s=e.capabilityEvidence["hard-write"],v=e.capabilityEvidence["advisory-write"],u=e.capabilityEvidence["merge-gate"],c=e.enforcementLadder.localWrite,t=typeof c.operationCovered=="boolean",l=t?c.operationCovered:n,p=t&&l===!0,d=!!(o&&r.installed&&p&&c.hard===!0),f=t?p&&r.installed:o&&s.length>0&&r.installed?n:!1,b=i&&v.length>0&&r.installed?n:!1,y=!!(e.ci?.failClosed&&u.length>0),C=y&&r.installed?n:!1;return{schemaVersion:"1.1",activeHost:e.activeHost,localWrite:g({supported:o,configuredPaths:s,installed:r,active:f,runtimeObserved:t,operation:t?c.operation??null:null,operationCoverage:l,bypassable:d?!1:o&&!t?n:!0,required:n,hard:d,sources:{active:t?"observed PreToolUse attempt":"runtime observation unavailable",runtimeObserved:t?"fresh PreToolUse invocation":"runtime observation unavailable",operationCoverage:t?"active-host operation matcher":"operation not observed",bypassable:d?"observed hard write boundary":"host runtime bypass evidence unavailable",required:"local host policy unavailable",hard:d?"fresh covered active-host invocation":"hardness not proven for this invocation"}}),advisoryMcp:g({supported:i,configuredPaths:v,installed:r,active:b,runtimeObserved:!1,operation:null,operationCoverage:n,bypassable:!0,required:n,hard:!1,sources:{active:"MCP runtime observation unavailable",runtimeObserved:"doctor did not observe an MCP tool invocation",operationCoverage:"advisory MCP is caller-invoked",bypassable:"advisory MCP does not intercept every write",required:"local host policy unavailable",hard:"MCP presence is advisory and never proves a hard boundary"}}),ciMerge:g({supported:!0,configuredPaths:y?u:[],installed:r,active:C,runtimeObserved:!1,operation:"merge",operationCoverage:y?n:!1,bypassable:y?n:!0,required:n,hard:!1,sources:{active:"CI run and provider enforcement not observed",runtimeObserved:"provider evidence unavailable",operationCoverage:"required-status operation coverage unavailable",bypassable:"branch-protection evidence unavailable",required:"branch-protection evidence unavailable",hard:"merge hardness requires fresh provider evidence"}})}}function $(a,e,r,o){return{...a,...o,evidence:[...a.evidence.filter(i=>!e.includes(i.field)),...e.map(i=>({field:i,source:r,value:o[i]}))]}}function R(a,e){if(!e)return a;const r=e.reason==="provider-policy-unavailable-plan"||e.policyReason==="unavailable-plan",o=e.available===!0,i=!o&&!r&&(e.reason==="provider-enforcement-unverified"||e.reason==="gh-cli-unavailable"||e.reason==="gh-repo-unavailable"||!!e.reason);if(!o&&e.runtimeObserved!==!0&&!r&&!i)return a;const s=o?typeof e.arkCheckRequired=="boolean"?e.arkCheckRequired:n:r?!1:n,v=!!(a.enforcementState.ciMerge.configured&&a.enforcementState.ciMerge.installed),u=s===!0?v:s===!1?!1:v?n:!1,c=u===!0?e.arkCheckSourceBound===!1?!0:n:s===!1?!0:v?n:!0,t=e.runtimeObserved===!0,l=o?`GitHub branch protection (${e.repo??"repository"}:${e.branch??"default"})`:r?`GitHub provider policy unavailable (plan) (${e.repo??"repository"}:${e.branch??"default"})`:`GitHub CI runtime (${e.repo??"repository"})`,p=s,d=u===!0&&c===!1&&p===!0,f=$(a.enforcementState.ciMerge,["active","runtimeObserved","operationCoverage","bypassable","required","hard"],l,{active:u,runtimeObserved:t,operationCoverage:p,bypassable:c,required:s,hard:d}),b={...a,enforcementState:{...a.enforcementState,ciMerge:f},enforcementLadder:{...a.enforcementLadder,ciMerge:{...a.enforcementLadder.ciMerge,requiredStatus:s,...e.latestCiRun?{latestCiRun:e.latestCiRun}:{},...r?{providerPolicy:"unavailable-plan"}:{}}}};return(r||e.reason)&&(b.providerEnforcement={available:o,reason:e.reason||(r?"provider-policy-unavailable-plan":"provider-enforcement-unverified"),policyReason:e.policyReason||(r?"unavailable-plan":null),runtimeObserved:t,latestCiRun:e.latestCiRun??null,hard:d===!0}),b}function k(a,e){const r=o=>o===!0?"yes":o===!1?"no":String(o);return`${a} \u2014 supported: ${r(e.supported)} \xB7 analyzed: ${r(e.analyzed)} \xB7 configured: ${r(e.configured)} \xB7 installed: ${r(e.installed)} \xB7 runtime observed: ${r(e.runtimeObserved)} \xB7 operation: ${e.operation??"none"} \xB7 operation covered: ${r(e.operationCoverage)} \xB7 active: ${r(e.active)} \xB7 bypassable: ${r(e.bypassable)} \xB7 required: ${r(e.required)} \xB7 hard: ${r(e.hard)}`}function x(a){const e=[{level:a.localWrite.active===!0?"ok":"warn",text:k("Local write",a.localWrite)},{level:"warn",text:k("Advisory MCP",a.advisoryMcp)},{level:a.ciMerge.required===!0?"ok":"warn",text:k("CI merge",a.ciMerge)}];return a.localWrite.active===n&&a.localWrite.hard===!1&&e.push({level:"bad",text:"RED FLAG: local hook assets exist, but this active-host operation was not observed at runtime; hard blocking is unverified."}),a.activeHost==="unknown"&&e.push({level:"warn",text:"Active host unknown for this invocation \u2014 enforcementState is session projection only. See writePath.inventory for on-disk host hooks; hard write is never claimed without runtime proof."}),e}export{O as buildEnforcementState,x as enforcementDoctorLines,S as packageInstallation,R as withCiProviderEvidence};
@@ -221,6 +221,22 @@ export function readDeclaredArkgatePin(root) {
221
221
  }
222
222
  }
223
223
 
224
+ /**
225
+ * True when this tree *is* the arkgate package (mother / self-host dogfood).
226
+ * Consumers never match: they depend on arkgate; they are not named arkgate with bin/.
227
+ * @param {string} root
228
+ */
229
+ export function isArkgateSelfHostRoot(root) {
230
+ try {
231
+ const pkgPath = path.join(root, 'package.json');
232
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
233
+ if (pkg?.name !== 'arkgate') return false;
234
+ return fs.statSync(path.join(root, 'bin', 'ark-check.mjs'), { throwIfNoEntry: false })?.isFile() === true;
235
+ } catch {
236
+ return false;
237
+ }
238
+ }
239
+
224
240
  /**
225
241
  * Dual-truth: CLI/package-shipped version vs consumer package.json pin.
226
242
  * Used by doctor + upgrade so agents never confuse managed-asset CLI with CI pin.
@@ -231,8 +247,9 @@ export function readDeclaredArkgatePin(root) {
231
247
  * dualTruth: boolean,
232
248
  * cliVersion: string|null,
233
249
  * declaredPin: string|null,
234
- * code: 'PACKAGE_PIN_BEHIND_CLI' | 'PACKAGE_PIN_MATCHES' | 'PACKAGE_PIN_ABSENT' | 'CLI_VERSION_UNKNOWN',
235
- * note: string
250
+ * code: 'PACKAGE_PIN_BEHIND_CLI' | 'PACKAGE_PIN_MATCHES' | 'PACKAGE_PIN_ABSENT' | 'PACKAGE_PIN_SELF_HOST' | 'CLI_VERSION_UNKNOWN',
251
+ * note: string,
252
+ * selfHost?: boolean,
236
253
  * }}
237
254
  */
238
255
  export function describePackageVersionDualTruth(root, opts = {}) {
@@ -241,6 +258,7 @@ export function describePackageVersionDualTruth(root, opts = {}) {
241
258
  ? opts.cliVersion
242
259
  : arkPackageVersion();
243
260
  const declaredPin = readDeclaredArkgatePin(root);
261
+ const selfHost = isArkgateSelfHostRoot(root);
244
262
  if (!cliVersion) {
245
263
  return {
246
264
  dualTruth: false,
@@ -248,15 +266,28 @@ export function describePackageVersionDualTruth(root, opts = {}) {
248
266
  declaredPin,
249
267
  code: 'CLI_VERSION_UNKNOWN',
250
268
  note: 'Could not read shipped arkgate package version for this CLI.',
269
+ selfHost,
251
270
  };
252
271
  }
253
272
  if (!declaredPin) {
273
+ // Mother / library author tree: package IS arkgate — no consumer pin required.
274
+ if (selfHost || process.env.ARK_MOTHER_CLI === '1' || process.env.ARK_MOTHER_CLI === 'true') {
275
+ return {
276
+ dualTruth: false,
277
+ cliVersion,
278
+ declaredPin: null,
279
+ code: 'PACKAGE_PIN_SELF_HOST',
280
+ note: 'This tree is arkgate itself (self-host); no consumer package.json pin is required.',
281
+ selfHost: true,
282
+ };
283
+ }
254
284
  return {
255
285
  dualTruth: false,
256
286
  cliVersion,
257
287
  declaredPin: null,
258
288
  code: 'PACKAGE_PIN_ABSENT',
259
289
  note: 'No arkgate pin in package.json; CI/npx may not resolve this CLI version.',
290
+ selfHost: false,
260
291
  };
261
292
  }
262
293
  // Normalize ^x.y.z / ~x.y.z / x.y.z for comparison of leading version token.
@@ -294,6 +325,7 @@ export function describePackageVersionDualTruth(root, opts = {}) {
294
325
  declaredPin,
295
326
  code: 'PACKAGE_PIN_BEHIND_CLI',
296
327
  note: `Managed CLI is arkgate@${cliVersion} but package.json pins ${declaredPin}. Bump the pin or re-run install so CI resolves the same version (common after upgrade --no-install).`,
328
+ selfHost,
297
329
  };
298
330
  }
299
331
  return {
@@ -302,6 +334,7 @@ export function describePackageVersionDualTruth(root, opts = {}) {
302
334
  declaredPin,
303
335
  code: 'PACKAGE_PIN_MATCHES',
304
336
  note: `package.json pin ${declaredPin} is aligned with CLI arkgate@${cliVersion}.`,
337
+ selfHost,
305
338
  };
306
339
  }
307
340