arkgate 4.8.14 → 4.8.15
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 +133 -0
- package/README.md +13 -5
- package/bin/ark-check-runtime.mjs +17 -49
- package/bin/ark-mcp-runtime.mjs +111 -2
- package/bin/ark-shared.mjs +140 -11
- package/bin/ark.mjs +7 -25
- package/bin/lib/adr-presence.mjs +97 -0
- package/bin/lib/agent-skills-package.mjs +179 -1
- package/bin/lib/analysis-completeness.mjs +38 -2
- package/bin/lib/analysis-engine.mjs +6 -6
- package/bin/lib/architecture-scan.mjs +26 -4
- package/bin/lib/ark-run-doctor.mjs +6 -0
- package/bin/lib/ark-run-report.mjs +6 -2
- package/bin/lib/arkrules-sensors.mjs +81 -0
- package/bin/lib/baseline-key.mjs +4 -1
- package/bin/lib/check-args.mjs +52 -2
- package/bin/lib/config-contract.mjs +64 -1
- package/bin/lib/diagnostic-catalog.mjs +3 -1
- package/bin/lib/doctor-advisories.mjs +94 -10
- package/bin/lib/doctor-human.mjs +18 -5
- package/bin/lib/doctor-next-actions.mjs +14 -0
- package/bin/lib/doctor-plan.mjs +32 -2
- package/bin/lib/enforcement-honesty.mjs +47 -2
- package/bin/lib/first-run-help.mjs +8 -1
- package/bin/lib/host-support-matrix.mjs +9 -1
- package/bin/lib/html-report-depth.mjs +2 -0
- package/bin/lib/html-report.mjs +13 -2
- package/bin/lib/import-resolve.mjs +74 -13
- package/bin/lib/improvement-compass-doctor.mjs +6 -1
- package/bin/lib/improvement-compass-map.mjs +3 -1
- package/bin/lib/layer-description.mjs +90 -8
- package/bin/lib/mcp-hook-payload.mjs +56 -0
- package/bin/lib/no-domain-frontend.mjs +91 -0
- package/bin/lib/presets.mjs +3 -4
- package/bin/lib/product-copy.mjs +15 -0
- package/bin/lib/projected-governed-coverage.mjs +114 -0
- package/bin/lib/recommend-cli.mjs +54 -0
- package/bin/lib/remediation.mjs +4 -0
- package/bin/lib/resolved-candidate-facts.mjs +83 -66
- package/bin/lib/rules-under-contract.mjs +117 -2
- package/bin/lib/snippet-analysis.mjs +14 -8
- package/bin/lib/start-preview.mjs +5 -2
- package/bin/lib/states-transitions-presence.mjs +212 -0
- package/bin/lib/status-command.mjs +2 -0
- package/bin/lib/status-transition-catalog.mjs +410 -0
- package/bin/lib/team-parliament-io.mjs +10 -0
- package/bin/lib/violations.mjs +13 -1
- package/bin/lib/write-path-capabilities.mjs +20 -5
- package/bin/lib/write-path-detect.mjs +27 -2
- package/dist/{configTypes-j7so8B4O.d.ts → configTypes-Dt3DpVbd.d.ts} +19 -0
- package/dist/{diagnosticCatalog-DVx_2RmF.d.ts → diagnosticCatalog-BEg85XlE.d.ts} +3 -3
- package/dist/eslint/index.cjs +4 -4
- package/dist/eslint/index.d.ts +1 -1
- package/dist/eslint/index.js +4 -4
- package/dist/index.cjs +31 -31
- package/dist/index.d.ts +70 -9
- package/dist/index.js +31 -31
- package/dist/nestjs/index.cjs +5 -5
- package/dist/nestjs/index.d.ts +3 -3
- package/dist/nestjs/index.js +5 -5
- package/dist/runtime/index.cjs +15 -15
- package/dist/runtime/index.d.ts +6 -6
- package/dist/runtime/index.js +15 -15
- package/dist/{types-Djbs3KjE.d.ts → types-CN9tVMPz.d.ts} +3 -1
- package/dist/{types-tGhZUiGX.d.ts → types-TBiv0WHL.d.ts} +1 -1
- package/docs/README.md +1 -1
- package/docs/agent-guide.md +6 -4
- package/docs/ai-gates.md +10 -3
- package/docs/brownfield-adoption.md +4 -1
- package/docs/configuration.md +55 -7
- package/docs/develop.md +1 -0
- package/docs/diagnostics.md +23 -3
- package/docs/enthusiast/how-to-agent-gates.md +5 -0
- package/docs/enthusiast/how-to-pick-shape.md +1 -1
- package/docs/package-surface.md +7 -5
- package/docs/use.md +5 -0
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +23 -2
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +82 -1
- package/templates/agent-skills/ark-autopilot/SKILL.md +6 -2
- package/templates/agent-skills/ark-coverage/SKILL.md +1 -1
- package/templates/agent-skills/ark-explain/SKILL.md +4 -2
- package/templates/agent-skills/ark-explore/SKILL.md +14 -1
- package/templates/agent-skills/ark-place/SKILL.md +38 -3
- package/templates/agent-skills/ark-runtime/SKILL.md +4 -2
- package/templates/agent-skills/ark-upgrade/SKILL.md +1 -1
- package/templates/arkrules/DomainModel.json +14 -1
- package/templates/skills/ark-adopt.md +82 -1
- package/templates/skills/ark-autopilot.md +6 -2
- package/templates/skills/ark-coverage.md +1 -1
- package/templates/skills/ark-explain.md +4 -2
- package/templates/skills/ark-explore.md +14 -1
- package/templates/skills/ark-place.md +38 -3
- package/templates/skills/ark-runtime.md +4 -2
- package/templates/skills/ark-upgrade.md +1 -1
|
@@ -34,6 +34,9 @@ export function collectDoctorNextActions(ctx) {
|
|
|
34
34
|
) {
|
|
35
35
|
actions.push(nudge.nextAction);
|
|
36
36
|
}
|
|
37
|
+
if (ctx.layerOwners?.required && ctx.layerOwners.nextAction) {
|
|
38
|
+
actions.push(ctx.layerOwners.nextAction);
|
|
39
|
+
}
|
|
37
40
|
const enforceEmptyPlan =
|
|
38
41
|
ctx.operatingMode === 'enforce' && planAEmpty && gatesInstalled && !notAdopted;
|
|
39
42
|
if (enforceEmptyPlan) {
|
|
@@ -42,6 +45,17 @@ export function collectDoctorNextActions(ctx) {
|
|
|
42
45
|
'/ark-explore, then one small refactor with /ark-autopilot and your OK'
|
|
43
46
|
);
|
|
44
47
|
}
|
|
48
|
+
if (ctx.adrPresence?.missing && ctx.adrPresence.nextAction) {
|
|
49
|
+
actions.push(ctx.adrPresence.nextAction);
|
|
50
|
+
}
|
|
51
|
+
if (ctx.statusTransitionCatalog?.nextAction) {
|
|
52
|
+
actions.push(ctx.statusTransitionCatalog.nextAction);
|
|
53
|
+
} else if (ctx.statesTransitions?.nextAction) {
|
|
54
|
+
actions.push(ctx.statesTransitions.nextAction);
|
|
55
|
+
}
|
|
56
|
+
if (ctx.noDomainFrontend?.nextAction) {
|
|
57
|
+
actions.push(ctx.noDomainFrontend.nextAction);
|
|
58
|
+
}
|
|
45
59
|
if (!ctx.analysisComplete) actions.push('restore complete analysis, then rerun ark-check --doctor');
|
|
46
60
|
if (ctx.designSmells.length > 0 && ctx.postGreenPath) actions.push(ctx.postGreenPath.action);
|
|
47
61
|
if (ctx.coverageHonesty.greenIsNotEnforcement && ctx.coverageHonesty.worseThanNoGate) {
|
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -15,7 +15,12 @@ import { describePackageVersionDualTruth } from './field-install.mjs';
|
|
|
15
15
|
import { detectAgentHomeGaps } from './agent-homes.mjs';
|
|
16
16
|
import { collectDoctorNextActions } from './doctor-next-actions.mjs';
|
|
17
17
|
import { printDoctorCompactHuman, printDoctorDetailsHuman } from './doctor-human.mjs';
|
|
18
|
-
import { placementDescriptionFields } from './layer-description.mjs';
|
|
18
|
+
import { collectLayerOwnerResidual, layerGuidanceLine, placementDescriptionFields } from './layer-description.mjs';
|
|
19
|
+
import { collectAdrPresenceResidual, printAdrPresenceHint } from './adr-presence.mjs';
|
|
20
|
+
import { collectStatesTransitionsResidual } from './states-transitions-presence.mjs';
|
|
21
|
+
import { collectStatusTransitionCatalogResidual } from './status-transition-catalog.mjs';
|
|
22
|
+
import { collectNoDomainFrontendResidual } from './no-domain-frontend.mjs';
|
|
23
|
+
export { printAdrPresenceHint };
|
|
19
24
|
export { printDoctorCompactHuman, printDoctorDetailsHuman };
|
|
20
25
|
export { summarizeRulesUnderContract };
|
|
21
26
|
|
|
@@ -173,7 +178,8 @@ export function runCoverage(root, config, files, rules, asJson) {
|
|
|
173
178
|
console.log(` ${pad('Layer')} Files`);
|
|
174
179
|
for (const row of layerRows) {
|
|
175
180
|
const flag = row.files === 0 ? ' (pattern matches nothing)' : '';
|
|
176
|
-
const
|
|
181
|
+
const guidance = layerGuidanceLine(row);
|
|
182
|
+
const caption = guidance ? ` ${guidance}` : '';
|
|
177
183
|
console.log(` ${pad(row.name)} ${String(row.files).padStart(5)}${flag}${caption}`);
|
|
178
184
|
}
|
|
179
185
|
console.log(` ${pad('(unclassified)')} ${String(unclassified.length).padStart(5)}`);
|
|
@@ -616,6 +622,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
616
622
|
options.facts ?? options.architectureFacts,
|
|
617
623
|
activeViolations
|
|
618
624
|
);
|
|
625
|
+
const layerOwners = collectLayerOwnerResidual(config);
|
|
619
626
|
const rulesUnderContract = doctorAdvisories.rulesUnderContract;
|
|
620
627
|
const arkRun = doctorAdvisories.arkRun;
|
|
621
628
|
// Single residual expression (nextPilot || extractionCard) — HTML report uses the same.
|
|
@@ -636,6 +643,13 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
636
643
|
github: githubForBoundary,
|
|
637
644
|
ci: ciMergeBoundary?.ci,
|
|
638
645
|
});
|
|
646
|
+
const adrPresence = collectAdrPresenceResidual({
|
|
647
|
+
root,
|
|
648
|
+
demanded: options.requireGates === true || adopted === 'required-merge',
|
|
649
|
+
});
|
|
650
|
+
const statesTransitions = collectStatesTransitionsResidual({ root });
|
|
651
|
+
const statusTransitionCatalog = collectStatusTransitionCatalogResidual({ root, config, files, statesTransitions });
|
|
652
|
+
const noDomainFrontend = collectNoDomainFrontendResidual({ config, coverage: cov, designSmells });
|
|
639
653
|
const { coverageHonesty, baselineHonesty, writePathHonesty, productHonesty } =
|
|
640
654
|
computeDoctorEnforcementHonesty({
|
|
641
655
|
governedPercent: cov.governed.percent,
|
|
@@ -671,6 +685,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
671
685
|
selfHost:
|
|
672
686
|
packageVersionTruth?.selfHost === true ||
|
|
673
687
|
packageVersionTruth?.code === 'PACKAGE_PIN_SELF_HOST',
|
|
688
|
+
nativeFailClosed: writePath.nativeFailClosed,
|
|
689
|
+
nativeFailClosedPolicy: writePath.nativeFailClosedPolicy,
|
|
674
690
|
adopted,
|
|
675
691
|
ciMergeBoundary,
|
|
676
692
|
github: githubForBoundary,
|
|
@@ -763,6 +779,10 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
763
779
|
governed: cov.governed,
|
|
764
780
|
coverageHonesty,
|
|
765
781
|
layers: cov.layers,
|
|
782
|
+
...(layerOwners ? { layerOwners } : {}),
|
|
783
|
+
...(adrPresence ? { adrPresence } : {}),
|
|
784
|
+
...(statesTransitions ? { statesTransitions } : {}),
|
|
785
|
+
...(statusTransitionCatalog ? { statusTransitionCatalog } : {}), ...(noDomainFrontend ? { noDomainFrontend } : {}),
|
|
766
786
|
emptyLayers: cov.emptyLayers,
|
|
767
787
|
layersWithoutRules: cov.layersWithoutRules,
|
|
768
788
|
ungovernedDirs: cov.suggestions.length,
|
|
@@ -827,6 +847,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
827
847
|
mcpPresent: writePath.mcpPresent,
|
|
828
848
|
evidence: writePath.evidence,
|
|
829
849
|
honesty: writePathHonesty,
|
|
850
|
+
nativeFailClosed: writePath.nativeFailClosed ?? null,
|
|
851
|
+
nativeFailClosedPolicy: writePath.nativeFailClosedPolicy ?? 'unsupported',
|
|
830
852
|
...(writePath.sessionNote ? { sessionNote: writePath.sessionNote } : {}),
|
|
831
853
|
...(writePath.gap
|
|
832
854
|
? {
|
|
@@ -902,12 +924,20 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
902
924
|
operatingMode,
|
|
903
925
|
adopted,
|
|
904
926
|
stewardNudge: doctorAdvisories.stewardNudge,
|
|
927
|
+
layerOwners,
|
|
928
|
+
adrPresence,
|
|
929
|
+
statesTransitions,
|
|
930
|
+
statusTransitionCatalog, noDomainFrontend,
|
|
905
931
|
});
|
|
906
932
|
const humanView = {
|
|
907
933
|
root,
|
|
908
934
|
analysisComplete,
|
|
909
935
|
completeness,
|
|
910
936
|
doctorAdvisories,
|
|
937
|
+
layerOwners,
|
|
938
|
+
adrPresence,
|
|
939
|
+
statesTransitions,
|
|
940
|
+
statusTransitionCatalog, noDomainFrontend,
|
|
911
941
|
operatingMode,
|
|
912
942
|
designFitness,
|
|
913
943
|
adopted,
|
|
@@ -180,6 +180,8 @@ export function buildBaselineHonesty(input = {}) {
|
|
|
180
180
|
* packagePinAbsent?: boolean,
|
|
181
181
|
* selfHost?: boolean,
|
|
182
182
|
* motherCli?: boolean,
|
|
183
|
+
* nativeFailClosed?: boolean | null,
|
|
184
|
+
* nativeFailClosedPolicy?: 'required' | 'unsupported' | 'none',
|
|
183
185
|
* }} [extras]
|
|
184
186
|
*/
|
|
185
187
|
export function buildWritePathHonesty(activeHost, hardWriteActive = false, extras = {}) {
|
|
@@ -202,7 +204,21 @@ export function buildWritePathHonesty(activeHost, hardWriteActive = false, extra
|
|
|
202
204
|
const hardAllowed = packageInstalled && !pinAbsentForUser;
|
|
203
205
|
const effectiveHard =
|
|
204
206
|
Boolean(hardWriteActive) && hardCapable && !softWriteHost && hardAllowed;
|
|
205
|
-
const
|
|
207
|
+
const nativeFailClosedPolicy =
|
|
208
|
+
extras.nativeFailClosedPolicy === 'required' ||
|
|
209
|
+
extras.nativeFailClosedPolicy === 'none' ||
|
|
210
|
+
extras.nativeFailClosedPolicy === 'unsupported'
|
|
211
|
+
? extras.nativeFailClosedPolicy
|
|
212
|
+
: HOST_SUPPORT_MATRIX[host]?.nativeFailClosed ?? (hardCapable ? 'unsupported' : 'none');
|
|
213
|
+
const nativeFailClosed =
|
|
214
|
+
extras.nativeFailClosed === true ? true : extras.nativeFailClosed === false ? false : null;
|
|
215
|
+
const failOpen = nativeFailClosedPolicy === 'required' && nativeFailClosed === false;
|
|
216
|
+
const honestyMessage = doctorWritePathHonestyMessage(host, effectiveHard);
|
|
217
|
+
const failOpenMessage =
|
|
218
|
+
`${HOST_SUPPORT_MATRIX[host]?.label ?? 'This host'}: the write hook is fail-open. ` +
|
|
219
|
+
'If the checker cannot run, the write still lands. Set failClosed: true on the write hook — ' +
|
|
220
|
+
'same idea as a file permission: no checker, no write. Required CI is the shared merge line.';
|
|
221
|
+
const message = failOpen ? failOpenMessage : honestyMessage;
|
|
206
222
|
|
|
207
223
|
/** @type {Record<string, unknown>} */
|
|
208
224
|
const out = {
|
|
@@ -212,6 +228,8 @@ export function buildWritePathHonesty(activeHost, hardWriteActive = false, extra
|
|
|
212
228
|
hardWriteSupported: hardCapable,
|
|
213
229
|
hardWriteActive: effectiveHard,
|
|
214
230
|
hardWriteUnverified: hardCapable && !effectiveHard,
|
|
231
|
+
nativeFailClosed,
|
|
232
|
+
nativeFailClosedPolicy,
|
|
215
233
|
hardMergeBoundary:
|
|
216
234
|
'required-github-status-context (CLI: arkgate-check --strict-merge / ark-check --strict-merge)',
|
|
217
235
|
packageInstalled,
|
|
@@ -385,6 +403,16 @@ export function buildProductHonesty(input = {}) {
|
|
|
385
403
|
});
|
|
386
404
|
}
|
|
387
405
|
|
|
406
|
+
if (write?.nativeFailClosed === false && write?.nativeFailClosedPolicy === 'required') {
|
|
407
|
+
reasons.push({
|
|
408
|
+
id: 'native-fail-open',
|
|
409
|
+
bucket: 'environment',
|
|
410
|
+
message:
|
|
411
|
+
write.message ||
|
|
412
|
+
'The write hook is fail-open. If the checker cannot run, the write still lands. Set failClosed: true on the write hook.',
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
388
416
|
const adopted =
|
|
389
417
|
typeof input.adopted === 'string'
|
|
390
418
|
? input.adopted
|
|
@@ -442,7 +470,7 @@ export function buildProductHonesty(input = {}) {
|
|
|
442
470
|
}
|
|
443
471
|
|
|
444
472
|
// EH05: environment residual deny-list (future reason ids stay architecture debt by default).
|
|
445
|
-
const ENVIRONMENT_REASON_IDS = new Set(['soft-write-host']);
|
|
473
|
+
const ENVIRONMENT_REASON_IDS = new Set(['soft-write-host', 'native-fail-open']);
|
|
446
474
|
|
|
447
475
|
const environmentResiduals = reasons.filter((r) => ENVIRONMENT_REASON_IDS.has(r.id));
|
|
448
476
|
const architectureReasons = reasons.filter((r) => !ENVIRONMENT_REASON_IDS.has(r.id));
|
|
@@ -457,6 +485,8 @@ export function buildProductHonesty(input = {}) {
|
|
|
457
485
|
|
|
458
486
|
const softWriteOnly =
|
|
459
487
|
!unfinished && environmentResiduals.some((r) => r.id === 'soft-write-host');
|
|
488
|
+
const failOpenOnly =
|
|
489
|
+
!unfinished && environmentResiduals.some((r) => r.id === 'native-fail-open');
|
|
460
490
|
const hostLabel = (() => {
|
|
461
491
|
const h = typeof write?.activeHost === 'string' ? write.activeHost.trim().toLowerCase() : '';
|
|
462
492
|
if (h === 'codex') return 'Codex';
|
|
@@ -493,6 +523,10 @@ export function buildProductHonesty(input = {}) {
|
|
|
493
523
|
'This tree acked advisory-only in .ark/adoption-stance.json. That is not a required GitHub merge status.';
|
|
494
524
|
} else if (softWriteOnly) {
|
|
495
525
|
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).`;
|
|
526
|
+
} else if (failOpenOnly) {
|
|
527
|
+
primaryMessage =
|
|
528
|
+
write?.message ||
|
|
529
|
+
`${hostLabel} write hook is fail-open — if the checker cannot run, the write still lands. Set failClosed: true. Required CI is the shared merge line.`;
|
|
496
530
|
} else if (wholeTreeGoverned) {
|
|
497
531
|
primaryMessage =
|
|
498
532
|
'No residual honesty blockers on this slice — still not a numeric architecture score; re-doctor after material change.';
|
|
@@ -509,6 +543,10 @@ export function buildProductHonesty(input = {}) {
|
|
|
509
543
|
headline = wholeTreeGoverned
|
|
510
544
|
? `Architecture contract ready; ${hostLabel} local writes are advisory`
|
|
511
545
|
: `Contract residual clear; ${hostLabel} local writes are advisory`;
|
|
546
|
+
} else if (!unfinished && failOpenOnly) {
|
|
547
|
+
headline = wholeTreeGoverned
|
|
548
|
+
? `Architecture contract ready; ${hostLabel} write hook is fail-open`
|
|
549
|
+
: `Contract residual clear; ${hostLabel} write hook is fail-open`;
|
|
512
550
|
} else if (!unfinished && adopted === 'advisory-only-acked') {
|
|
513
551
|
headline = 'Advisory-only adoption — merge status is not required';
|
|
514
552
|
} else if (!unfinished) {
|
|
@@ -536,6 +574,9 @@ export function buildProductHonesty(input = {}) {
|
|
|
536
574
|
primaryNextAction =
|
|
537
575
|
input.stewardNudge?.nextAction ||
|
|
538
576
|
'/ark-adopt (ask, then update stewards[] — do not invent names)';
|
|
577
|
+
} else if (!primaryNextAction && failOpenOnly) {
|
|
578
|
+
primaryNextAction =
|
|
579
|
+
'Set failClosed: true on the write hook, then re-run doctor. Required CI is the shared merge line.';
|
|
539
580
|
} else if (!primaryNextAction && softWriteOnly) {
|
|
540
581
|
primaryNextAction =
|
|
541
582
|
'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.';
|
|
@@ -613,6 +654,8 @@ export function computeDoctorEnforcementHonesty({
|
|
|
613
654
|
adoptionStance,
|
|
614
655
|
emptyStewards,
|
|
615
656
|
stewardNudge,
|
|
657
|
+
nativeFailClosed,
|
|
658
|
+
nativeFailClosedPolicy,
|
|
616
659
|
} = {}) {
|
|
617
660
|
const coverageHonesty = buildCoverageHonesty({
|
|
618
661
|
percent: governedPercent,
|
|
@@ -632,6 +675,8 @@ export function computeDoctorEnforcementHonesty({
|
|
|
632
675
|
packagePinAbsent: packageVersionTruth?.code === 'PACKAGE_PIN_ABSENT',
|
|
633
676
|
selfHost,
|
|
634
677
|
motherCli,
|
|
678
|
+
nativeFailClosed,
|
|
679
|
+
nativeFailClosedPolicy,
|
|
635
680
|
});
|
|
636
681
|
// Prefer explicit blocking count; fall back to activeViolations only when callers
|
|
637
682
|
// already pass blocking-only totals (legacy tests). Type-only must not invent debt.
|
|
@@ -11,6 +11,7 @@ ${NORTH_STAR_ONE_LINE}
|
|
|
11
11
|
|
|
12
12
|
arkgate start preview (no writes)
|
|
13
13
|
arkgate start --apply write host + CI setup
|
|
14
|
+
(refuses weak coverage/shape; lock with --archetype/--preset/--force)
|
|
14
15
|
arkgate-check --doctor status — one next step
|
|
15
16
|
|
|
16
17
|
Stuck? Run status (--doctor). Do #1.
|
|
@@ -167,6 +168,7 @@ export function checkUsage() {
|
|
|
167
168
|
NORTH_STAR_ONE_LINE,
|
|
168
169
|
'',
|
|
169
170
|
' arkgate-check --doctor where you are: one status light, one next action',
|
|
171
|
+
' arkgate-check --local --base <ref> optional local / multi-worktree cheap check',
|
|
170
172
|
' arkgate-check --changed --base <ref> local / pre-push: checks touched files only',
|
|
171
173
|
' arkgate-check --strict-merge CI / merge gate (required GitHub status)',
|
|
172
174
|
' arkgate-check --sensors which sensors can ever be enforced (does not run analysis)',
|
|
@@ -183,7 +185,7 @@ export function checkUsageAll() {
|
|
|
183
185
|
'',
|
|
184
186
|
'Usage: arkgate-check | ark-check (identical bins; product name ArkGate)',
|
|
185
187
|
' arkgate-check --version',
|
|
186
|
-
' 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]',
|
|
188
|
+
' 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] [--local] [--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]',
|
|
187
189
|
' 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',
|
|
188
190
|
' ark-check --coverage [--json] per-layer file counts + full unclassified list (report only, exit 0)',
|
|
189
191
|
' ark-check --plan [--json] classified remediation plan (mechanical-safe / judgment / deferred) + goal; report only',
|
|
@@ -225,6 +227,9 @@ export function checkUsageAll() {
|
|
|
225
227
|
'',
|
|
226
228
|
'Team parliament: law files (ark.config / arkrules / .ark-baseline.json) cannot ship in',
|
|
227
229
|
'the same diff as product source. --changed --base <ref> checks touched files only.',
|
|
230
|
+
'--local (or ARK_CHECK_LOCAL=1) is the same cheap path: it turns on --changed and',
|
|
231
|
+
'stays per worktree root. It cannot combine with --strict-merge. Write hooks stay',
|
|
232
|
+
'on the lexical snippet path; they do not run a full-tree check.',
|
|
228
233
|
'--against <ref> ratchets new keys vs that ref\'s baseline. --contract-session is a',
|
|
229
234
|
'steward law-only PR. Loosen / baseline-grow need stewards[] + --author when set.',
|
|
230
235
|
'',
|
|
@@ -279,6 +284,8 @@ export function checkUsageAll() {
|
|
|
279
284
|
'--require-gates implies --strict-config and fails when the Ark contract in AGENTS.md,',
|
|
280
285
|
'the project-rooted Ark server in .mcp.json, or fail-closed CI is missing/invalid.',
|
|
281
286
|
'Included but unclassified source files therefore stay red instead of false-green.',
|
|
287
|
+
'If gates are required and there is no short decision note yet, it points at docs/adr/',
|
|
288
|
+
'(or docs/decisions/) — a hint, not a fail. Off without --require-gates.',
|
|
282
289
|
'',
|
|
283
290
|
'--install-agent-gates writes AGENTS.md, .mcp.json, and the CI workflow for every',
|
|
284
291
|
'project, plus tool-specific templates. Known tools: claude, cursor, codex, grok, antigravity',
|
|
@@ -35,6 +35,11 @@ function hostProfile(label, hookPath, hookSurface, hookOperations, hardWrite, re
|
|
|
35
35
|
'repair-envelope-emitted': repairEnvelopeEmitted,
|
|
36
36
|
'repair-reinjection-guaranteed': repairReinjectionGuaranteed,
|
|
37
37
|
}),
|
|
38
|
+
// Host-native "deny when the checker cannot run" flag (FS-permission analog).
|
|
39
|
+
// required = schema field must be true for hard-write evidence (Cursor failClosed).
|
|
40
|
+
// unsupported = host has no documented flag; hook-crash behavior is host-owned.
|
|
41
|
+
// none = advisory host; no local write block.
|
|
42
|
+
nativeFailClosed: extras.nativeFailClosed ?? (hardWrite ? 'unsupported' : 'none'),
|
|
38
43
|
// EH07 minimum ops matrix (hard=false for soft hosts on every listed op).
|
|
39
44
|
operationCoverage: Object.freeze(
|
|
40
45
|
extras.operationCoverage ||
|
|
@@ -83,6 +88,7 @@ export const HOST_SUPPORT_MATRIX = Object.freeze({
|
|
|
83
88
|
false,
|
|
84
89
|
{
|
|
85
90
|
repairEnvelopeEmitted: true,
|
|
91
|
+
nativeFailClosed: 'required',
|
|
86
92
|
operationCoverage: {
|
|
87
93
|
Write: true,
|
|
88
94
|
StrReplace: true,
|
|
@@ -186,7 +192,9 @@ ${rows}
|
|
|
186
192
|
**Read the CI column:** for every host, the repository-wide hard guarantee is a **required**
|
|
187
193
|
GitHub **status context** that runs the CLI — not “CI file present,” and not the CLI binary name alone.
|
|
188
194
|
Codex hard write covers only a complete local \`apply_patch\`; Cursor covers only listed
|
|
189
|
-
\`preToolUse\` ops
|
|
195
|
+
\`preToolUse\` ops with \`failClosed: true\`. A Cursor hook without that flag is fail-open
|
|
196
|
+
(if the checker cannot run, the write still lands). In both cases the project hook must be
|
|
197
|
+
installed + trusted, while shell/direct
|
|
190
198
|
filesystem writes, hosted or specialized opt-out paths, and human edits still rely on CI.
|
|
191
199
|
|
|
192
200
|
This table describes the supported profile **after its files are installed and the host loads/trusts them**. A hard local boundary covers only the listed hook operations; alternate tools, direct filesystem writes, and human edits still rely on CI. MCP validation is advisory because the agent must call it. The CI check blocks a merge only when the repository makes that status required. Repair **envelopes** may be emitted without reinjection being guaranteed; silent auto-apply never happens. Run \`arkgate-check --doctor\` (or \`ark-check --doctor\`) for the evidence actually detected in the current repository.`;
|
|
@@ -127,6 +127,8 @@ export function buildReportDepthPayload(
|
|
|
127
127
|
selfHost:
|
|
128
128
|
packageVersionTruth?.selfHost === true ||
|
|
129
129
|
packageVersionTruth?.code === 'PACKAGE_PIN_SELF_HOST',
|
|
130
|
+
nativeFailClosed: writePath?.nativeFailClosed,
|
|
131
|
+
nativeFailClosedPolicy: writePath?.nativeFailClosedPolicy,
|
|
130
132
|
});
|
|
131
133
|
const classification = {
|
|
132
134
|
governedPercent: coverage?.governed?.percent ?? null,
|
package/bin/lib/html-report.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import { capabilityBadgesFor, renderAdvisorySections } from './html-report-advis
|
|
|
25
25
|
import { renderEvolutionSection } from './html-report-evolution.mjs';
|
|
26
26
|
import { arkGitignoreAppendDecision } from './ark-gitignore.mjs';
|
|
27
27
|
import { captureGitSnapshot } from './report-snapshot-context.mjs';
|
|
28
|
-
import { layerDescriptionCaption } from './layer-description.mjs';
|
|
28
|
+
import { layerDescriptionCaption, layerGuidanceLine, layerOwners, layerTrustBoundary } from './layer-description.mjs';
|
|
29
29
|
|
|
30
30
|
export { arkGitignoreAppendDecision, gitignoreCoversArkState, gitignoreHasArkNegationException } from './ark-gitignore.mjs';
|
|
31
31
|
|
|
@@ -423,8 +423,13 @@ export function renderBeginnerHtmlReport({ root, config, violations, ok, version
|
|
|
423
423
|
const placementRows = layers
|
|
424
424
|
.map((layer) => {
|
|
425
425
|
const purpose = layerDescriptionCaption(layer) || 'See ark.config.json';
|
|
426
|
+
const guidance = layerGuidanceLine(layer);
|
|
427
|
+
const extra = guidance && guidance !== purpose ? guidance.slice(purpose.length).replace(/^ · /, '') : '';
|
|
428
|
+
const purposeCell = extra
|
|
429
|
+
? `${esc(purpose)} <span class="tag">${esc(extra)}</span>`
|
|
430
|
+
: esc(purpose);
|
|
426
431
|
const folders = (layer.patterns || []).join(', ') || '—';
|
|
427
|
-
return `<tr><td><strong>${esc(layer.name)}</strong></td><td>${
|
|
432
|
+
return `<tr><td><strong>${esc(layer.name)}</strong></td><td>${purposeCell}</td><td><code>${esc(folders)}</code></td></tr>`;
|
|
428
433
|
})
|
|
429
434
|
.join('\n');
|
|
430
435
|
|
|
@@ -776,6 +781,12 @@ export function renderHtmlReport({
|
|
|
776
781
|
? `<span class="tag">${layer.intentPrefixes.map(esc).join(' ')}</span>`
|
|
777
782
|
: '',
|
|
778
783
|
layer.optional ? '<span class="tag dim-tag">optional</span>' : '',
|
|
784
|
+
layerTrustBoundary(layer)
|
|
785
|
+
? `<span class="tag">trust: ${esc(layerTrustBoundary(layer))}</span>`
|
|
786
|
+
: '',
|
|
787
|
+
layerOwners(layer)
|
|
788
|
+
? `<span class="tag">owner: ${esc(layerOwners(layer).map((id) => (id.includes('@') ? id : `@${id}`)).join(', '))}</span>`
|
|
789
|
+
: '',
|
|
779
790
|
].join(' ');
|
|
780
791
|
const example = exampleByLayer?.get?.(layer.name);
|
|
781
792
|
const files = counts.get(layer.name) || 0;
|
|
@@ -4,7 +4,46 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
layerForRelativePath,
|
|
9
|
+
matchingLayersForRelativePath,
|
|
10
|
+
} from '../ark-layer-match.mjs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Same candidate list as ark-check `resolveSpecifier` (src/kernel/moduleGraph.ts).
|
|
14
|
+
* The write hook must feed `layerForRelativePath` a path with the extension
|
|
15
|
+
* ark-check already sees on disk — otherwise an explicit `money.ts` pattern
|
|
16
|
+
* loses to a broader `src/lib/**` bag.
|
|
17
|
+
*/
|
|
18
|
+
const SPECIFIER_SUFFIXES = Object.freeze([
|
|
19
|
+
'',
|
|
20
|
+
'.ts',
|
|
21
|
+
'.tsx',
|
|
22
|
+
'.mts',
|
|
23
|
+
'.cts',
|
|
24
|
+
'/index.ts',
|
|
25
|
+
'/index.tsx',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const SOURCE_EXT = /\.(?:[cm]?[jt]sx?)$/i;
|
|
29
|
+
|
|
30
|
+
function posixRel(value) {
|
|
31
|
+
return String(value).split(/[/\\]/).join('/');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function specifierRelCandidates(rel) {
|
|
35
|
+
const base = posixRel(rel);
|
|
36
|
+
if (SOURCE_EXT.test(base)) return [base];
|
|
37
|
+
return SPECIFIER_SUFFIXES.map((suffix) => `${base}${suffix}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isOnDiskFile(root, rel) {
|
|
41
|
+
try {
|
|
42
|
+
return fs.statSync(path.join(root, rel)).isFile();
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
8
47
|
|
|
9
48
|
/**
|
|
10
49
|
* Read tsconfig path aliases via the TypeScript config parser (JSONC + extends).
|
|
@@ -86,16 +125,38 @@ function filePathToRel(filePath, root) {
|
|
|
86
125
|
return relative.split(path.sep).join('/');
|
|
87
126
|
}
|
|
88
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Classify a specifier-relative path with the same specificity scorer as ark-check.
|
|
130
|
+
* Prefer an on-disk candidate; otherwise pick the candidate whose winning
|
|
131
|
+
* `layerForRelativePath` pattern scores highest (explicit file beats `src/lib/**`).
|
|
132
|
+
*/
|
|
89
133
|
function classifyProbe(root, rel, layers) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
134
|
+
const candidates = specifierRelCandidates(rel);
|
|
135
|
+
const existing = candidates.find((candidate) => isOnDiskFile(root, candidate));
|
|
136
|
+
if (existing) {
|
|
137
|
+
return {
|
|
138
|
+
relPath: existing,
|
|
139
|
+
layer: layerForRelativePath(existing, layers),
|
|
140
|
+
onDisk: true,
|
|
141
|
+
};
|
|
95
142
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
143
|
+
let bestRel = posixRel(rel);
|
|
144
|
+
let bestLayer;
|
|
145
|
+
let bestScore = -1;
|
|
146
|
+
for (const candidate of candidates) {
|
|
147
|
+
const layer = layerForRelativePath(candidate, layers);
|
|
148
|
+
if (!layer) continue;
|
|
149
|
+
const hit = matchingLayersForRelativePath(candidate, layers).find(
|
|
150
|
+
(row) => row.layer === layer
|
|
151
|
+
);
|
|
152
|
+
const score = hit?.score ?? -1;
|
|
153
|
+
if (score > bestScore) {
|
|
154
|
+
bestScore = score;
|
|
155
|
+
bestLayer = layer;
|
|
156
|
+
bestRel = candidate;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { relPath: bestRel, layer: bestLayer, onDisk: false };
|
|
99
160
|
}
|
|
100
161
|
|
|
101
162
|
/**
|
|
@@ -114,7 +175,7 @@ export function createImportTargetResolver(ts, root, config) {
|
|
|
114
175
|
if (path.isAbsolute(specifierOrFilePath)) {
|
|
115
176
|
const relPath = filePathToRel(specifierOrFilePath, root);
|
|
116
177
|
if (!relPath) return undefined;
|
|
117
|
-
return
|
|
178
|
+
return classifyProbe(root, relPath, layers);
|
|
118
179
|
}
|
|
119
180
|
|
|
120
181
|
// Relative or path-alias import
|
|
@@ -130,7 +191,7 @@ export function createImportTargetResolver(ts, root, config) {
|
|
|
130
191
|
tsAliases
|
|
131
192
|
);
|
|
132
193
|
if (!rel) return undefined;
|
|
133
|
-
return
|
|
194
|
+
return classifyProbe(root, rel, layers);
|
|
134
195
|
}
|
|
135
196
|
|
|
136
197
|
// Try as import alias / bare package first
|
|
@@ -141,13 +202,13 @@ export function createImportTargetResolver(ts, root, config) {
|
|
|
141
202
|
tsAliases
|
|
142
203
|
);
|
|
143
204
|
if (asImport) {
|
|
144
|
-
return
|
|
205
|
+
return classifyProbe(root, asImport, layers);
|
|
145
206
|
}
|
|
146
207
|
|
|
147
208
|
// Repo-relative source file path (not an import specifier)
|
|
148
209
|
const asFile = filePathToRel(specifierOrFilePath, root);
|
|
149
210
|
if (asFile) {
|
|
150
|
-
return
|
|
211
|
+
return classifyProbe(root, asFile, layers);
|
|
151
212
|
}
|
|
152
213
|
|
|
153
214
|
return undefined;
|
|
@@ -42,7 +42,12 @@ export function buildDoctorImprovementCompass(input = {}) {
|
|
|
42
42
|
if (id.includes('PEER_ISOLATION')) peerIsolationCount += 1;
|
|
43
43
|
if (id === 'CAPABILITY_VIOLATION') pureOrCapabilityResidual += 1;
|
|
44
44
|
if (id === 'FORBIDDEN_GLOBAL' || id.startsWith('FORBIDDEN_')) forbiddenGlobalResidual += 1;
|
|
45
|
-
if (
|
|
45
|
+
if (
|
|
46
|
+
id.startsWith('ARKRULE_') ||
|
|
47
|
+
id === 'INVARIANT_UNCOVERED' ||
|
|
48
|
+
id === 'INVARIANT_CATALOG_EMPTY'
|
|
49
|
+
)
|
|
50
|
+
arkRulesStructureResidual += 1;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
const pcFindings = input.physicalCohesion?.findings;
|
|
@@ -260,7 +260,9 @@ function mapViolations(byId, violations) {
|
|
|
260
260
|
});
|
|
261
261
|
continue;
|
|
262
262
|
}
|
|
263
|
-
if (upper.startsWith('ARKRULE_') ||
|
|
263
|
+
if (upper.startsWith('ARKRULE_') ||
|
|
264
|
+
upper === 'INVARIANT_UNCOVERED' ||
|
|
265
|
+
upper === 'INVARIANT_CATALOG_EMPTY') {
|
|
264
266
|
attach('encapsulation', 'ArkRules structure / invariant residual inside a layer.', {
|
|
265
267
|
kind: 'skill',
|
|
266
268
|
ref: '/ark-autopilot',
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* App-context caption from
|
|
2
|
+
* App-context caption, optional trust tag, and optional owners from layer metadata.
|
|
3
3
|
* Metadata only — callers project it; policyHash strips it elsewhere.
|
|
4
|
-
* Present
|
|
5
|
-
|
|
4
|
+
* Present values are returned; absence/empty/invalid is omitted.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const LAYER_TRUST_BOUNDARIES = Object.freeze(['public', 'auth', 'admin', 'internal']);
|
|
8
|
+
|
|
9
|
+
/**
|
|
6
10
|
* @param {{ description?: unknown } | null | undefined} layerOrPlacement
|
|
7
11
|
* @returns {string | undefined}
|
|
8
12
|
*/
|
|
@@ -15,13 +19,91 @@ export function layerDescriptionCaption(layerOrPlacement) {
|
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
22
|
+
* @param {{ trustBoundary?: unknown } | null | undefined} layerOrPlacement
|
|
23
|
+
* @returns {'public' | 'auth' | 'admin' | 'internal' | undefined}
|
|
24
|
+
*/
|
|
25
|
+
export function layerTrustBoundary(layerOrPlacement) {
|
|
26
|
+
const tag =
|
|
27
|
+
layerOrPlacement && typeof layerOrPlacement === 'object'
|
|
28
|
+
? layerOrPlacement.trustBoundary
|
|
29
|
+
: undefined;
|
|
30
|
+
return typeof tag === 'string' && LAYER_TRUST_BOUNDARIES.includes(tag) ? tag : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {{ owners?: unknown } | null | undefined} layerOrPlacement
|
|
35
|
+
* @returns {string[] | undefined}
|
|
36
|
+
*/
|
|
37
|
+
export function layerOwners(layerOrPlacement) {
|
|
38
|
+
const raw =
|
|
39
|
+
layerOrPlacement && typeof layerOrPlacement === 'object' ? layerOrPlacement.owners : undefined;
|
|
40
|
+
if (!Array.isArray(raw) || raw.length === 0) return undefined;
|
|
41
|
+
const ids = raw.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
42
|
+
return ids.length > 0 ? ids : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatOwnerMention(id) {
|
|
46
|
+
return id.includes('@') ? id : `@${id}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* One guidance fragment: caption and/or `trust: public` and/or `owner: @handle`.
|
|
51
|
+
* @param {{ description?: unknown, trustBoundary?: unknown, owners?: unknown } | null | undefined} layerOrPlacement
|
|
52
|
+
* @returns {string | undefined}
|
|
53
|
+
*/
|
|
54
|
+
export function layerGuidanceLine(layerOrPlacement) {
|
|
55
|
+
const caption = layerDescriptionCaption(layerOrPlacement);
|
|
56
|
+
const trust = layerTrustBoundary(layerOrPlacement);
|
|
57
|
+
const owners = layerOwners(layerOrPlacement);
|
|
58
|
+
const ownerBit = owners ? `owner: ${owners.map(formatOwnerMention).join(', ')}` : undefined;
|
|
59
|
+
const bits = [caption, trust ? `trust: ${trust}` : undefined, ownerBit].filter(Boolean);
|
|
60
|
+
return bits.length > 0 ? bits.join(' · ') : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Project caption + trust tag + owners onto place / prepare-write / coverage / doctor JSON.
|
|
65
|
+
* Absence omits the field (never empty string / empty array).
|
|
20
66
|
*
|
|
21
|
-
* @param {{ description?: unknown } | null | undefined} layerOrPlacement
|
|
22
|
-
* @returns {{ description
|
|
67
|
+
* @param {{ description?: unknown, trustBoundary?: unknown, owners?: unknown } | null | undefined} layerOrPlacement
|
|
68
|
+
* @returns {{ description?: string, trustBoundary?: string, owners?: string[] }}
|
|
23
69
|
*/
|
|
24
70
|
export function placementDescriptionFields(layerOrPlacement) {
|
|
25
71
|
const caption = layerDescriptionCaption(layerOrPlacement);
|
|
26
|
-
|
|
72
|
+
const trust = layerTrustBoundary(layerOrPlacement);
|
|
73
|
+
const owners = layerOwners(layerOrPlacement);
|
|
74
|
+
return {
|
|
75
|
+
...(caption ? { description: caption } : {}),
|
|
76
|
+
...(trust ? { trustBoundary: trust } : {}),
|
|
77
|
+
...(owners ? { owners } : {}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Doctor residual when requireLayerOwners is on and a live house has no owners.
|
|
83
|
+
* Absent/false require → null (silent). Not a computeDoctorAdvisories key.
|
|
84
|
+
*
|
|
85
|
+
* @param {{ requireLayerOwners?: unknown, layers?: Array<{ name?: string, owners?: unknown, optional?: boolean, reserved?: boolean, allowEmpty?: boolean }> } | null | undefined} config
|
|
86
|
+
* @returns {{ required: true, missingLayers: string[], ask: string, nextAction: string } | null}
|
|
87
|
+
*/
|
|
88
|
+
export function collectLayerOwnerResidual(config) {
|
|
89
|
+
if (config?.requireLayerOwners !== true || !Array.isArray(config.layers)) return null;
|
|
90
|
+
const missing = config.layers
|
|
91
|
+
.filter(
|
|
92
|
+
(layer) =>
|
|
93
|
+
layer &&
|
|
94
|
+
layer.optional !== true &&
|
|
95
|
+
layer.reserved !== true &&
|
|
96
|
+
layer.allowEmpty !== true &&
|
|
97
|
+
!layerOwners(layer)
|
|
98
|
+
)
|
|
99
|
+
.map((layer) => layer.name)
|
|
100
|
+
.filter((name) => typeof name === 'string' && name.length > 0);
|
|
101
|
+
if (missing.length === 0) return null;
|
|
102
|
+
const named = missing[0];
|
|
103
|
+
return {
|
|
104
|
+
required: true,
|
|
105
|
+
missingLayers: missing,
|
|
106
|
+
ask: `${named} has no owner. Add a GitHub handle or email to layers[].owners.`,
|
|
107
|
+
nextAction: `Add a GitHub handle or email to ${named}'s owners in ark.config.json (/ark-adopt).`,
|
|
108
|
+
};
|
|
27
109
|
}
|