arkgate 4.6.5 → 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 +72 -2106
- package/README.md +11 -9
- package/bin/ark-check-runtime.mjs +36 -332
- package/bin/ark-mcp-runtime.mjs +7 -323
- package/bin/ark-shared.mjs +24 -158
- package/bin/ark.mjs +13 -3
- package/bin/lib/adoption-stance.mjs +104 -0
- package/bin/lib/check-args.mjs +173 -0
- package/bin/lib/check-config-detect.mjs +101 -0
- package/bin/lib/check-watch.mjs +80 -0
- package/bin/lib/ci-merge-boundary.mjs +4 -2
- package/bin/lib/deep-module-coach.mjs +3 -0
- package/bin/lib/design-delta.mjs +2 -2
- package/bin/lib/design-smells.mjs +1 -1
- package/bin/lib/diagnostic-catalog.mjs +1 -1
- package/bin/lib/doctor-advisories.mjs +2 -2
- package/bin/lib/doctor-human.mjs +509 -0
- package/bin/lib/doctor-next-actions.mjs +20 -2
- package/bin/lib/doctor-plan.mjs +86 -456
- package/bin/lib/enforcement-honesty.mjs +70 -0
- package/bin/lib/first-run-help.mjs +8 -7
- package/bin/lib/github-enforcement.mjs +22 -9
- package/bin/lib/html-report-advisories.mjs +10 -2
- package/bin/lib/html-report.mjs +26 -9
- package/bin/lib/mcp-adoption.mjs +19 -0
- package/bin/lib/mcp-hook-payload.mjs +328 -0
- package/bin/lib/package-manager.mjs +174 -0
- package/bin/lib/policy-delta-io.mjs +5 -1
- package/bin/lib/post-green-path.mjs +5 -1
- package/bin/lib/product-copy.mjs +6 -3
- package/bin/lib/start-preview.mjs +12 -22
- package/bin/lib/status-command.mjs +16 -0
- package/bin/lib/status-manifest.mjs +8 -2
- package/bin/lib/team-parliament-io.mjs +66 -2
- package/bin/lib/team-parliament.mjs +25 -5
- package/bin/lib/unavailable-analysis.mjs +1 -0
- package/dist/index.cjs +2 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +2 -2
- package/docs/README.md +6 -10
- package/docs/ai-gates.md +12 -5
- package/docs/configuration.md +9 -1
- package/docs/diagnostics.md +2 -2
- package/docs/package-surface.md +6 -4
- package/docs/product-voice.md +6 -4
- package/docs/threat-model.md +2 -2
- package/docs/use.md +5 -4
- package/package.json +1 -1
- package/schemas/ark.design-delta.schema.json +1 -1
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
HOST_SUPPORT_MATRIX,
|
|
11
11
|
HOST_SUPPORT_HOSTS,
|
|
12
12
|
} from './host-support-matrix.mjs';
|
|
13
|
+
import {
|
|
14
|
+
classifyAdopted,
|
|
15
|
+
MERGE_BOUNDARY_NOT_REQUIRED,
|
|
16
|
+
NOT_ADOPTED_NEXT_ACTION,
|
|
17
|
+
} from './adoption-stance.mjs';
|
|
13
18
|
|
|
14
19
|
/** Soft = matrix hard-write false; hard-capable = matrix hard-write true. Single source of truth. */
|
|
15
20
|
function hostWriteClassSets() {
|
|
@@ -380,6 +385,37 @@ export function buildProductHonesty(input = {}) {
|
|
|
380
385
|
});
|
|
381
386
|
}
|
|
382
387
|
|
|
388
|
+
const adopted =
|
|
389
|
+
typeof input.adopted === 'string'
|
|
390
|
+
? input.adopted
|
|
391
|
+
: input.ciMergeBoundary || input.github || input.adoptionStance || input.considerMergeBoundary
|
|
392
|
+
? classifyAdopted({
|
|
393
|
+
stance: input.adoptionStance,
|
|
394
|
+
github: input.github,
|
|
395
|
+
ci: input.ciMergeBoundary?.ci,
|
|
396
|
+
})
|
|
397
|
+
: null;
|
|
398
|
+
if (adopted === 'not-adopted') {
|
|
399
|
+
reasons.push({
|
|
400
|
+
id: MERGE_BOUNDARY_NOT_REQUIRED,
|
|
401
|
+
message:
|
|
402
|
+
'Merge boundary not adopted — require a GitHub status running arkgate-check --strict-merge, or write .ark/adoption-stance.json with stance: "advisory-only".',
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const emptyStewards =
|
|
407
|
+
input.emptyStewards === true ||
|
|
408
|
+
input.stewardNudge?.emptyStewardsPastGrace === true ||
|
|
409
|
+
(input.stewardNudge?.needsStewards === true &&
|
|
410
|
+
(input.stewardNudge?.stewardCount ?? 0) === 0);
|
|
411
|
+
if (emptyStewards) {
|
|
412
|
+
reasons.push({
|
|
413
|
+
id: 'empty-stewards',
|
|
414
|
+
message:
|
|
415
|
+
'No stewards listed — not a finished Enforce. Name GitHub handles or emails for stewards[], or this stays Adapt-or-nudge. /ark-adopt asks; it does not invent names.',
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
383
419
|
// Mode adapt/suggest (FG-FINISHED-ADAPT-DEBT): prefer unfinished unless the tree is
|
|
384
420
|
// whole-tree green AND zero design smells AND zero blocking violations.
|
|
385
421
|
// Type-only placement debt alone must not keep adapt unfinished via active-blocking.
|
|
@@ -442,6 +478,8 @@ export function buildProductHonesty(input = {}) {
|
|
|
442
478
|
architectureReasons.find((r) => r.id === 'package-pin-absent') ||
|
|
443
479
|
architectureReasons.find((r) => r.id === 'baseline-missing-with-debt') ||
|
|
444
480
|
architectureReasons.find((r) => r.id === 'residual-pilot') ||
|
|
481
|
+
architectureReasons.find((r) => r.id === MERGE_BOUNDARY_NOT_REQUIRED) ||
|
|
482
|
+
architectureReasons.find((r) => r.id === 'empty-stewards') ||
|
|
445
483
|
architectureReasons[0] ||
|
|
446
484
|
environmentResiduals[0];
|
|
447
485
|
|
|
@@ -450,6 +488,9 @@ export function buildProductHonesty(input = {}) {
|
|
|
450
488
|
primaryMessage =
|
|
451
489
|
primary?.message ||
|
|
452
490
|
'Not finished: residual honesty signals remain (violations, mode, coverage, freeze, design, package pin, or pilots).';
|
|
491
|
+
} else if (adopted === 'advisory-only-acked') {
|
|
492
|
+
primaryMessage =
|
|
493
|
+
'This tree acked advisory-only in .ark/adoption-stance.json. That is not a required GitHub merge status.';
|
|
453
494
|
} else if (softWriteOnly) {
|
|
454
495
|
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
496
|
} else if (wholeTreeGoverned) {
|
|
@@ -468,6 +509,8 @@ export function buildProductHonesty(input = {}) {
|
|
|
468
509
|
headline = wholeTreeGoverned
|
|
469
510
|
? `Architecture contract ready; ${hostLabel} local writes are advisory`
|
|
470
511
|
: `Contract residual clear; ${hostLabel} local writes are advisory`;
|
|
512
|
+
} else if (!unfinished && adopted === 'advisory-only-acked') {
|
|
513
|
+
headline = 'Advisory-only adoption — merge status is not required';
|
|
471
514
|
} else if (!unfinished) {
|
|
472
515
|
headline = 'Honesty clear on residual signals';
|
|
473
516
|
} else if (coverageIncomplete) {
|
|
@@ -487,6 +530,12 @@ export function buildProductHonesty(input = {}) {
|
|
|
487
530
|
} else if (!primaryNextAction && pinAbsent) {
|
|
488
531
|
primaryNextAction =
|
|
489
532
|
'Add arkgate to package.json and install so CI/npx resolve this CLI (PACKAGE_PIN_ABSENT)';
|
|
533
|
+
} else if (!primaryNextAction && adopted === 'not-adopted') {
|
|
534
|
+
primaryNextAction = NOT_ADOPTED_NEXT_ACTION;
|
|
535
|
+
} else if (!primaryNextAction && emptyStewards) {
|
|
536
|
+
primaryNextAction =
|
|
537
|
+
input.stewardNudge?.nextAction ||
|
|
538
|
+
'/ark-adopt (ask, then update stewards[] — do not invent names)';
|
|
490
539
|
} else if (!primaryNextAction && softWriteOnly) {
|
|
491
540
|
primaryNextAction =
|
|
492
541
|
'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.';
|
|
@@ -558,6 +607,12 @@ export function computeDoctorEnforcementHonesty({
|
|
|
558
607
|
packageInstalled,
|
|
559
608
|
selfHost,
|
|
560
609
|
motherCli,
|
|
610
|
+
adopted: adoptedInput,
|
|
611
|
+
ciMergeBoundary,
|
|
612
|
+
github,
|
|
613
|
+
adoptionStance,
|
|
614
|
+
emptyStewards,
|
|
615
|
+
stewardNudge,
|
|
561
616
|
} = {}) {
|
|
562
617
|
const coverageHonesty = buildCoverageHonesty({
|
|
563
618
|
percent: governedPercent,
|
|
@@ -583,6 +638,14 @@ export function computeDoctorEnforcementHonesty({
|
|
|
583
638
|
const blockingForHonesty = Number.isFinite(Number(activeBlockingViolations))
|
|
584
639
|
? Math.max(0, Number(activeBlockingViolations))
|
|
585
640
|
: Number(activeViolations) || 0;
|
|
641
|
+
const adopted =
|
|
642
|
+
typeof adoptedInput === 'string'
|
|
643
|
+
? adoptedInput
|
|
644
|
+
: classifyAdopted({
|
|
645
|
+
stance: adoptionStance,
|
|
646
|
+
github,
|
|
647
|
+
ci: ciMergeBoundary?.ci,
|
|
648
|
+
});
|
|
586
649
|
const productHonesty = buildProductHonesty({
|
|
587
650
|
coverageHonesty,
|
|
588
651
|
baselineHonesty,
|
|
@@ -598,6 +661,13 @@ export function computeDoctorEnforcementHonesty({
|
|
|
598
661
|
primaryNextAction,
|
|
599
662
|
operatingMode,
|
|
600
663
|
activeBlockingViolations: blockingForHonesty,
|
|
664
|
+
adopted,
|
|
665
|
+
ciMergeBoundary,
|
|
666
|
+
github,
|
|
667
|
+
adoptionStance,
|
|
668
|
+
considerMergeBoundary: true,
|
|
669
|
+
emptyStewards,
|
|
670
|
+
stewardNudge,
|
|
601
671
|
});
|
|
602
672
|
return {
|
|
603
673
|
coverageHonesty,
|
|
@@ -5,14 +5,11 @@
|
|
|
5
5
|
export function setupUsage() {
|
|
6
6
|
return `arkgate (alias ark) — One architecture config. One check. One coach.
|
|
7
7
|
|
|
8
|
-
arkgate start preview
|
|
9
|
-
arkgate start --apply write
|
|
10
|
-
arkgate-check --doctor
|
|
8
|
+
arkgate start preview (no writes)
|
|
9
|
+
arkgate start --apply write host + CI setup
|
|
10
|
+
arkgate-check --doctor where you are + one next action
|
|
11
11
|
|
|
12
|
-
Then session 0 in your agent: /ark-adopt
|
|
13
12
|
Stuck? Run doctor. Do #1.
|
|
14
|
-
|
|
15
|
-
More commands and flags: arkgate --help --all
|
|
16
13
|
`;
|
|
17
14
|
}
|
|
18
15
|
|
|
@@ -114,7 +111,7 @@ export function checkUsageAll() {
|
|
|
114
111
|
'Usage: arkgate-check | ark-check (identical bins; product name ArkGate)',
|
|
115
112
|
' arkgate-check --version',
|
|
116
113
|
' arkgate-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict-merge | --strict | --strict-config] [--policy-base <file> | --policy-base-ref <git-ref>] [--policy-ack <file>] [--fail-on-new-smells --base-ref <git-ref>] [--contract-diff] [--contract-session] [--changed] [--against <git-ref>] [--base <git-ref>] [--persona touch|contributor|agent|steward] [--author <id>] [--require-gates] [--require-write-hook <host>] [--json] [--baseline [file]] [--report [file.html]] [--no-cache]',
|
|
117
|
-
' ark-check --doctor [--json] [--resident] [--fail-on-new-smells --base-ref <git-ref>]
|
|
114
|
+
' ark-check --doctor [--json] [--all] [--resident] [--fail-on-new-smells --base-ref <git-ref>] compact first screen; --all prints Details; resident JSON falls back cold',
|
|
118
115
|
' ark-check --coverage [--json] per-layer file counts + full unclassified list (report only, exit 0)',
|
|
119
116
|
' ark-check --plan [--json] classified remediation plan (mechanical-safe / judgment / deferred) + goal; report only',
|
|
120
117
|
' ark-check --rules-inventory [--json] brownfield rules inventory (AR13; deterministic candidates, not a score)',
|
|
@@ -183,6 +180,10 @@ export function checkUsageAll() {
|
|
|
183
180
|
'transition. Weakening or judgment-required findings fail unless --policy-ack names',
|
|
184
181
|
'every finding and is bound to both policy hashes. Use --policy-base/--policy-base-ref',
|
|
185
182
|
'for an explicit comparison; ARK_POLICY_BASE_REF is the CI environment equivalent.',
|
|
183
|
+
'The same merge profile blocks new UI business-rule files (domain-logic-in-ui) created',
|
|
184
|
+
'versus that base; leftover design on existing files stays green. Missing base skips',
|
|
185
|
+
'this check (does not exit 2). --fail-on-new-smells --base-ref remains the full',
|
|
186
|
+
'new+worsened-on-touched-paths ratchet.',
|
|
186
187
|
'Add --require-write-hook claude|grok|antigravity|cursor|codex to validate a hard local',
|
|
187
188
|
'write boundary for that specific host. Codex covers complete local apply_patch only;',
|
|
188
189
|
'hosted/specialized/direct-write paths and OpenCode remain CI-backed. Merge blocking requires',
|
|
@@ -3,6 +3,19 @@ import { spawnSync } from 'node:child_process';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
|
+
/** Kill hung gh instead of stalling CI. */
|
|
7
|
+
export const SPAWN_TIMEOUT_MS = 8000;
|
|
8
|
+
|
|
9
|
+
function runGh(args, opts = {}) {
|
|
10
|
+
const { run, ...rest } = opts;
|
|
11
|
+
const spawn = typeof run === 'function' ? run : spawnSync;
|
|
12
|
+
return spawn('gh', args, {
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
...rest,
|
|
15
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
6
19
|
const IF_LINE = /^[ \t]*(?:-\s+)?(?:"if"|'if'|if):\s*(.*?)\s*(?:#.*)?$/i;
|
|
7
20
|
const CONTINUE_LINE = /^[ \t]*(?:-\s+)?(?:"continue-on-error"|'continue-on-error'|continue-on-error):\s*(.*?)\s*(?:#.*)?$/i;
|
|
8
21
|
const SAFE_IF = /^(?:['"]?true['"]?|['"]?\$\{\{\s*(?:true|always\(\))\s*\}\}['"]?)$/i;
|
|
@@ -434,7 +447,7 @@ export function reportGithubCiRuntime(opts = {}) {
|
|
|
434
447
|
const cwd = opts.cwd ?? process.cwd();
|
|
435
448
|
const env = opts.env ?? process.env;
|
|
436
449
|
const limit = Number.isFinite(Number(opts.limit)) ? Math.max(1, Number(opts.limit)) : 30;
|
|
437
|
-
if (
|
|
450
|
+
if (runGh(['--version'], { env, run: opts.run }).status !== 0) {
|
|
438
451
|
return { runtimeObserved: false, latestCiRun: null, reason: 'gh-cli-unavailable' };
|
|
439
452
|
}
|
|
440
453
|
const args = [
|
|
@@ -443,7 +456,7 @@ export function reportGithubCiRuntime(opts = {}) {
|
|
|
443
456
|
'--json', 'name,conclusion,status,workflowName,displayTitle,event',
|
|
444
457
|
];
|
|
445
458
|
if (opts.repo) args.push('--repo', opts.repo);
|
|
446
|
-
const result =
|
|
459
|
+
const result = runGh(args, { cwd, env, run: opts.run });
|
|
447
460
|
if (result.status !== 0) {
|
|
448
461
|
const err = `${result.stderr || ''}${result.stdout || ''}`.slice(0, 400);
|
|
449
462
|
return {
|
|
@@ -499,14 +512,14 @@ export function reportGithubCiRuntime(opts = {}) {
|
|
|
499
512
|
export function reportGithubBranchProtection(opts = {}) {
|
|
500
513
|
const cwd = opts.cwd ?? process.cwd();
|
|
501
514
|
const env = opts.env ?? process.env;
|
|
502
|
-
if (
|
|
515
|
+
if (runGh(['--version'], { env, run: opts.run }).status !== 0) {
|
|
503
516
|
return { available: false, reason: 'gh-cli-unavailable', runtimeObserved: false, latestCiRun: null };
|
|
504
517
|
}
|
|
505
518
|
let repo = opts.repo;
|
|
506
519
|
let branch = opts.branch;
|
|
507
520
|
if (!repo || !branch) {
|
|
508
521
|
const args = ['repo', 'view', ...(repo ? [repo] : []), '--json', 'nameWithOwner,defaultBranchRef'];
|
|
509
|
-
const metadata = parseJson(
|
|
522
|
+
const metadata = parseJson(runGh(args, { cwd, env, run: opts.run }));
|
|
510
523
|
if (!metadata?.nameWithOwner || !metadata?.defaultBranchRef?.name) {
|
|
511
524
|
return { available: false, reason: 'gh-repo-unavailable', runtimeObserved: false, latestCiRun: null };
|
|
512
525
|
}
|
|
@@ -514,13 +527,13 @@ export function reportGithubBranchProtection(opts = {}) {
|
|
|
514
527
|
branch ??= metadata.defaultBranchRef.name;
|
|
515
528
|
}
|
|
516
529
|
|
|
517
|
-
const classicResult =
|
|
530
|
+
const classicResult = runGh([
|
|
518
531
|
'api', `repos/${repo}/branches/${encodeURIComponent(branch)}/protection`, '--jq',
|
|
519
532
|
'{strict: .required_status_checks.strict, contexts: .required_status_checks.contexts, checks: .required_status_checks.checks, enforcesAdmins: .enforce_admins.enabled}',
|
|
520
|
-
], { cwd,
|
|
521
|
-
const rulesResult =
|
|
522
|
-
|
|
523
|
-
{ cwd,
|
|
533
|
+
], { cwd, env, run: opts.run });
|
|
534
|
+
const rulesResult = runGh(
|
|
535
|
+
['api', `repos/${repo}/rules/branches/${encodeURIComponent(branch)}`],
|
|
536
|
+
{ cwd, env, run: opts.run }
|
|
524
537
|
);
|
|
525
538
|
const classic = parseJson(classicResult);
|
|
526
539
|
const rules = parseJson(rulesResult);
|
|
@@ -313,17 +313,25 @@ function deepModuleCoachHtml(coach) {
|
|
|
313
313
|
|
|
314
314
|
function stewardNudgeHtml(nudge) {
|
|
315
315
|
if (!nudge || nudge.notAScore !== true) return '';
|
|
316
|
+
const unfinished = Boolean(
|
|
317
|
+
nudge.emptyStewardsPastGrace || (nudge.needsStewards && (nudge.stewardCount ?? 0) === 0)
|
|
318
|
+
);
|
|
316
319
|
const ask =
|
|
317
|
-
(nudge.needsStewards || nudge.drift
|
|
320
|
+
(nudge.needsStewards || nudge.drift || nudge.emptyStewardsPastGrace) &&
|
|
321
|
+
typeof nudge.ask === 'string' &&
|
|
322
|
+
nudge.ask
|
|
318
323
|
? `<p>${esc(nudge.ask)}</p>`
|
|
319
324
|
: '<p class="muted">No steward list gap (advisory).</p>';
|
|
320
325
|
const next =
|
|
321
326
|
typeof nudge.nextAction === 'string' && nudge.nextAction
|
|
322
327
|
? `<p class="muted">Next: ${esc(nudge.nextAction)}</p>`
|
|
323
328
|
: '';
|
|
329
|
+
const qualifier = unfinished
|
|
330
|
+
? '(unfinished residual — changes finished, not check valid)'
|
|
331
|
+
: '(advisory — never changes the check valid bit)';
|
|
324
332
|
return `
|
|
325
333
|
<section class="section card" data-advisory="stewardNudge">
|
|
326
|
-
<h2>Stewards <span class="muted"
|
|
334
|
+
<h2>Stewards <span class="muted">${qualifier}</span></h2>
|
|
327
335
|
${ask}
|
|
328
336
|
${next}
|
|
329
337
|
<p class="muted">GitHub handle or email. Never invent names. Always <code>notAScore</code>.</p>
|
package/bin/lib/html-report.mjs
CHANGED
|
@@ -28,6 +28,9 @@ import { captureGitSnapshot } from './report-snapshot-context.mjs';
|
|
|
28
28
|
|
|
29
29
|
export { arkGitignoreAppendDecision, gitignoreCoversArkState, gitignoreHasArkNegationException } from './ark-gitignore.mjs';
|
|
30
30
|
|
|
31
|
+
/** Cap the rendered violation list so showcase HTML cannot dump unbounded findings. */
|
|
32
|
+
export const HTML_REPORT_VIOLATION_LIST_CAP = 12;
|
|
33
|
+
|
|
31
34
|
export function detectEnforcement(root) {
|
|
32
35
|
const has = (rel) => fs.existsSync(path.join(root, rel));
|
|
33
36
|
const fileIncludes = (rel, needle) => {
|
|
@@ -110,7 +113,7 @@ export function baselineSignalHint(signal) {
|
|
|
110
113
|
export function modeBadgeHint(mode) {
|
|
111
114
|
switch (String(mode || '').toLowerCase()) {
|
|
112
115
|
case 'enforce':
|
|
113
|
-
return 'Contract matches the tree
|
|
116
|
+
return 'Contract matches the tree on checked import edges. Required GitHub status (or an advisory-only ack) is what adopts the merge boundary.';
|
|
114
117
|
case 'adapt':
|
|
115
118
|
return 'Contract is live but still aligning (optional cores with files, empty cores, or presentation-bag false green).';
|
|
116
119
|
case 'suggest':
|
|
@@ -255,7 +258,7 @@ export function computeReportFitness({ coverage, violations, ok, enforcement, co
|
|
|
255
258
|
const modeBlurb = {
|
|
256
259
|
suggest: 'Starter shape — expand layers as the codebase grows.',
|
|
257
260
|
adapt: 'Contract is live; raise governed coverage or match real folders.',
|
|
258
|
-
enforce: 'Contract
|
|
261
|
+
enforce: 'Contract matches the tree on checked import edges. Merge is adopted only with a required GitHub status or an advisory-only ack.',
|
|
259
262
|
}[mode];
|
|
260
263
|
const scoreCoverage = governedPercent == null ? 50 : governedPercent;
|
|
261
264
|
const scoreClean =
|
|
@@ -392,9 +395,13 @@ export function renderBeginnerHtmlReport({ root, config, violations, ok, version
|
|
|
392
395
|
})
|
|
393
396
|
.join('\n');
|
|
394
397
|
|
|
398
|
+
const listedBeginner = violations.slice(0, HTML_REPORT_VIOLATION_LIST_CAP);
|
|
399
|
+
const hiddenBeginner = Math.max(0, violations.length - listedBeginner.length);
|
|
400
|
+
const beginnerRemainder = hiddenBeginner
|
|
401
|
+
? `<div class="dim">+${hiddenBeginner} more (${violations.length} total)</div>`
|
|
402
|
+
: '';
|
|
395
403
|
const violationRows = violations.length
|
|
396
|
-
?
|
|
397
|
-
.slice(0, 12)
|
|
404
|
+
? listedBeginner
|
|
398
405
|
.map((v) => {
|
|
399
406
|
const enriched = enrichViolationWithFixClass(v);
|
|
400
407
|
return `<li><code>${esc(v.file)}:${v.line}</code> — ${esc(enriched.enthusiastHint ?? v.message)}</li>`;
|
|
@@ -436,6 +443,7 @@ export function renderBeginnerHtmlReport({ root, config, violations, ok, version
|
|
|
436
443
|
</table>
|
|
437
444
|
<h2>What to fix first</h2>
|
|
438
445
|
<ul>${violationRows}</ul>
|
|
446
|
+
${beginnerRemainder}
|
|
439
447
|
<h2>Next steps</h2>
|
|
440
448
|
<p><code>${arkCheckCommand(root)}</code></p>
|
|
441
449
|
<p><code>${arkCommand(root, 'ark-check', '--recommend')}</code></p>
|
|
@@ -781,13 +789,22 @@ export function renderHtmlReport({
|
|
|
781
789
|
})
|
|
782
790
|
.join('\n');
|
|
783
791
|
|
|
784
|
-
const
|
|
792
|
+
const listedViolations = violations.slice(0, HTML_REPORT_VIOLATION_LIST_CAP);
|
|
793
|
+
const hiddenViolationCount = Math.max(0, violations.length - listedViolations.length);
|
|
794
|
+
const ruleTotals = new Map();
|
|
785
795
|
for (const v of violations) {
|
|
796
|
+
ruleTotals.set(v.ruleId, (ruleTotals.get(v.ruleId) || 0) + 1);
|
|
797
|
+
}
|
|
798
|
+
const byRule = new Map();
|
|
799
|
+
for (const v of listedViolations) {
|
|
786
800
|
if (!byRule.has(v.ruleId)) byRule.set(v.ruleId, []);
|
|
787
801
|
byRule.get(v.ruleId).push(v);
|
|
788
802
|
}
|
|
789
|
-
const
|
|
790
|
-
?
|
|
803
|
+
const remainderNote = hiddenViolationCount
|
|
804
|
+
? `<div class="dim">+${hiddenViolationCount} more (${violations.length} total)</div>`
|
|
805
|
+
: '';
|
|
806
|
+
const violationBlocks = listedViolations.length
|
|
807
|
+
? `${[...byRule.entries()]
|
|
791
808
|
.map(([ruleId, items]) => {
|
|
792
809
|
const hint = FIX_HINTS[ruleId];
|
|
793
810
|
const rows = items
|
|
@@ -803,12 +820,12 @@ export function renderHtmlReport({
|
|
|
803
820
|
})
|
|
804
821
|
.join('\n');
|
|
805
822
|
return `<div class="vgroup">
|
|
806
|
-
<div class="vghead"><span class="rule">${esc(ruleId)}</span> <span class="dim">${items.length}</span></div>
|
|
823
|
+
<div class="vghead"><span class="rule">${esc(ruleId)}</span> <span class="dim">${ruleTotals.get(ruleId) ?? items.length}</span></div>
|
|
807
824
|
<ul class="vitems">${rows}</ul>
|
|
808
825
|
${hint ? `<div class="fix">fix: ${esc(hint)}</div>` : ''}
|
|
809
826
|
</div>`;
|
|
810
827
|
})
|
|
811
|
-
.join('\n')
|
|
828
|
+
.join('\n')}${remainderNote}`
|
|
812
829
|
: `<div class="clean hero-clean">
|
|
813
830
|
<div class="clean-title">Architecture matches the contract</div>
|
|
814
831
|
<div class="clean-body">No active violations${suppressed ? ` · ${suppressed} frozen by baseline` : ''}. This is what “honest green” looks like when coverage is real.</div>
|
package/bin/lib/mcp-adoption.mjs
CHANGED
|
@@ -17,6 +17,11 @@ import { detectActiveAgentHost, skillTemplateNames } from './skill-install.mjs';
|
|
|
17
17
|
import { detectDeployPathQuality } from './deploy-path.mjs';
|
|
18
18
|
import { collectWeakestLinkGaps } from './weakest-link.mjs';
|
|
19
19
|
import { codexRuntimeActivation, withCiProviderEvidence } from './enforcement-state.mjs';
|
|
20
|
+
import {
|
|
21
|
+
classifyAdopted,
|
|
22
|
+
readAdoptionStance,
|
|
23
|
+
NOT_ADOPTED_NEXT_ACTION,
|
|
24
|
+
} from './adoption-stance.mjs';
|
|
20
25
|
|
|
21
26
|
export { detectDeployPathQuality };
|
|
22
27
|
|
|
@@ -528,6 +533,20 @@ export function collectAdoptionGaps(root, config, coverage) {
|
|
|
528
533
|
gaps.push(g);
|
|
529
534
|
}
|
|
530
535
|
|
|
536
|
+
const adoptedKind = classifyAdopted({
|
|
537
|
+
stance: readAdoptionStance(root),
|
|
538
|
+
github: weakest.github,
|
|
539
|
+
});
|
|
540
|
+
if (adoptedKind === 'not-adopted') {
|
|
541
|
+
gaps.push({
|
|
542
|
+
id: 'adoption-stance-missing',
|
|
543
|
+
severity: 'warn',
|
|
544
|
+
message:
|
|
545
|
+
'Merge boundary not adopted: require a GitHub status on arkgate-check --strict-merge, or write .ark/adoption-stance.json with stance: "advisory-only".',
|
|
546
|
+
fix: NOT_ADOPTED_NEXT_ACTION,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
531
550
|
return {
|
|
532
551
|
gaps,
|
|
533
552
|
hosts,
|