arkgate 4.6.4 → 4.6.6
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 +77 -2105
- package/README.md +11 -9
- package/bin/ark-check-runtime.mjs +136 -16
- package/bin/ark-mcp-runtime.mjs +18 -30
- package/bin/ark.mjs +13 -3
- package/bin/lib/adapter-contract.mjs +13 -9
- package/bin/lib/adoption-stance.mjs +104 -0
- package/bin/lib/agent-projection-command.mjs +18 -0
- package/bin/lib/agent-projection.mjs +2 -2
- package/bin/lib/analysis-engine.mjs +5 -5
- package/bin/lib/ci-and-commands.mjs +3 -3
- package/bin/lib/ci-merge-boundary.mjs +91 -0
- package/bin/lib/config-contract.mjs +2 -0
- package/bin/lib/design-delta.mjs +2 -2
- package/bin/lib/design-smells.mjs +1 -1
- package/bin/lib/diagnostic-catalog.mjs +6 -5
- package/bin/lib/doctor-advisories.mjs +2 -2
- package/bin/lib/doctor-next-actions.mjs +35 -5
- package/bin/lib/doctor-plan.mjs +164 -133
- package/bin/lib/enforcement-honesty.mjs +72 -0
- package/bin/lib/first-run-help.mjs +8 -7
- package/bin/lib/graph-blind.mjs +15 -6
- package/bin/lib/html-report-advisories.mjs +10 -2
- package/bin/lib/html-report.mjs +2 -2
- package/bin/lib/install-migrate.mjs +10 -0
- package/bin/lib/invariant-coverage.mjs +6 -2
- package/bin/lib/managed-upgrade.mjs +8 -3
- package/bin/lib/mcp-adoption.mjs +19 -0
- package/bin/lib/policy-delta-io.mjs +1 -1
- package/bin/lib/post-green-path.mjs +5 -1
- package/bin/lib/presets.mjs +22 -0
- package/bin/lib/product-copy.mjs +6 -3
- package/bin/lib/remediation.mjs +74 -10
- package/bin/lib/skill-install.mjs +2 -0
- package/bin/lib/snippet-analysis.mjs +40 -8
- 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 +62 -2
- package/bin/lib/team-parliament.mjs +25 -5
- package/bin/lib/unavailable-analysis.mjs +1 -0
- package/dist/{configTypes-B8uIcLaG.d.ts → configTypes-l6XiwiC1.d.ts} +7 -0
- package/dist/eslint/index.cjs +3 -3
- package/dist/eslint/index.d.ts +1 -1
- package/dist/eslint/index.js +3 -3
- package/dist/index.cjs +26 -26
- package/dist/index.d.ts +20 -3
- package/dist/index.js +29 -29
- package/docs/README.md +6 -9
- package/docs/agent-guide.md +10 -0
- package/docs/ai-gates.md +12 -5
- package/docs/brownfield-adoption.md +7 -1
- package/docs/configuration.md +11 -2
- package/docs/develop.md +4 -2
- package/docs/diagnostics.md +17 -7
- 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.config.schema.json +6 -0
- package/schemas/ark.design-delta.schema.json +1 -1
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +7 -0
- package/templates/agent-skills/ark-explore/SKILL.md +6 -0
- package/templates/agent-skills/ark-place/SKILL.md +11 -4
- package/templates/agent-skills/ark-upgrade/SKILL.md +9 -2
- package/templates/skills/ark-adopt.md +7 -0
- package/templates/skills/ark-explore.md +6 -0
- package/templates/skills/ark-place.md +11 -4
- package/templates/skills/ark-upgrade.md +9 -2
package/README.md
CHANGED
|
@@ -16,9 +16,11 @@ and makes sure a “green” check means something real.
|
|
|
16
16
|
|
|
17
17
|
</div>
|
|
18
18
|
|
|
19
|
-
> **ArkGate 4.6.
|
|
20
|
-
>
|
|
21
|
-
>
|
|
19
|
+
> **ArkGate 4.6.6** is current (this train). **4.6.5** remains npm `latest` until publish.
|
|
20
|
+
> A tree is **adopted** only with a required GitHub status running `arkgate-check --strict-merge`,
|
|
21
|
+
> or `.ark/adoption-stance.json` `stance: "advisory-only"`. Doctor is compact (`--doctor --all`
|
|
22
|
+
> for Details). [4.6.6 notes](docs/releases/4.6.6.md) · [4.6.5](docs/releases/4.6.5.md) ·
|
|
23
|
+
> [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md)
|
|
22
24
|
|
|
23
25
|
---
|
|
24
26
|
|
|
@@ -41,12 +43,11 @@ npm install -D arkgate typescript
|
|
|
41
43
|
npx arkgate start # preview files + commands
|
|
42
44
|
npx arkgate start --apply # compact contract + host router + CI plan
|
|
43
45
|
npx arkgate-check --doctor # control plane: status light + primary next action
|
|
46
|
+
npx arkgate-check --doctor --all # encyclopedia (Details)
|
|
44
47
|
```
|
|
45
48
|
|
|
46
49
|
That is the product. Doctor is the control plane — when stuck, do **primary next action #1**.
|
|
47
|
-
|
|
48
|
-
dependency inversion — **not a score**). Leftover lenses mean design work may remain even when
|
|
49
|
-
imports are green. Details: [use.md — Improvement compass](docs/use.md#improvement-compass-not-a-score).
|
|
50
|
+
JSON still carries improvement compass and coach (**not a score**). Compact human output does not.
|
|
50
51
|
|
|
51
52
|
```text
|
|
52
53
|
start → doctor (+ compass) → /ark-adopt (session 0) → day-to-day /ark-place
|
|
@@ -102,7 +103,7 @@ check: [4.3.0 — What ArkGate is / isn't](docs/releases/4.3.0.md#what-arkgate-i
|
|
|
102
103
|
|-------|--------|-----------|
|
|
103
104
|
| **Suggest** | Thin / new tree | Finish `start` → doctor |
|
|
104
105
|
| **Adapt** | Not fully protected | Doctor action #1 |
|
|
105
|
-
| **Enforce** | Honest edges
|
|
106
|
+
| **Enforce** | Honest import edges, and no new UI business-rule files vs merge-base | Keep write path + CI |
|
|
106
107
|
| **Enforce · design-weak** | Edges clean; design residual remains | Shape residual — not “done” |
|
|
107
108
|
|
|
108
109
|
Details: [docs/use.md](docs/use.md).
|
|
@@ -216,8 +217,9 @@ for real systems. Details: [docs/production-hardening.md](docs/production-harden
|
|
|
216
217
|
| Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
|
|
217
218
|
| Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
|
|
218
219
|
| Security | [SECURITY.md](SECURITY.md) |
|
|
219
|
-
| Current release (4.6.
|
|
220
|
-
|
|
|
220
|
+
| Current release (4.6.6) | [docs/releases/4.6.6.md](docs/releases/4.6.6.md) · [CHANGELOG](CHANGELOG.md) |
|
|
221
|
+
| Prior published (4.6.5 on npm `latest` until 4.6.6 lands) | [docs/releases/4.6.5.md](docs/releases/4.6.5.md) |
|
|
222
|
+
| Prior published (4.6.3) | [docs/releases/4.6.3.md](docs/releases/4.6.3.md) |
|
|
221
223
|
| Prior (4.6.2) | [docs/releases/4.6.2.md](docs/releases/4.6.2.md) |
|
|
222
224
|
| Prior (4.6.1) | [docs/releases/4.6.1.md](docs/releases/4.6.1.md) |
|
|
223
225
|
| Prior (4.6.0) | [docs/releases/4.6.0.md](docs/releases/4.6.0.md) |
|
|
@@ -87,9 +87,11 @@ import {
|
|
|
87
87
|
import {
|
|
88
88
|
ARCHITECTURE_PRESETS,
|
|
89
89
|
APPLICATION_LIB_ORCHESTRATION_PATTERNS,
|
|
90
|
+
COMPOSITION_ROOT_PATH_PATTERNS,
|
|
90
91
|
DOMAIN_PATH_PATTERNS,
|
|
91
92
|
NEXT_API_APPLICATION_PATTERNS,
|
|
92
93
|
PERSISTENCE_PATH_PATTERNS,
|
|
94
|
+
SHARED_KERNEL_PATH_PATTERNS,
|
|
93
95
|
retrofitP0aApiApplicationPatterns,
|
|
94
96
|
withDefaultArkRules,
|
|
95
97
|
writeArkRulesTemplates,
|
|
@@ -103,10 +105,15 @@ import { runArchitectureScan } from './lib/architecture-scan.mjs';
|
|
|
103
105
|
import { ANALYSIS_COMPLETENESS, analysisIncompleteStatement } from './lib/analysis-completeness.mjs';
|
|
104
106
|
import { reportUnavailableAnalysis } from './lib/unavailable-analysis.mjs';
|
|
105
107
|
import { validateHardWriteRequest } from './lib/enforcement-profiles.mjs';
|
|
106
|
-
import {
|
|
108
|
+
import {
|
|
109
|
+
analyzePolicyTransition,
|
|
110
|
+
discoverLocalBaseRef,
|
|
111
|
+
normalizePolicyBaseRef,
|
|
112
|
+
} from './lib/policy-delta-io.mjs';
|
|
107
113
|
import {
|
|
108
114
|
applyAgainstRatchet,
|
|
109
115
|
bindTeamBaseRefs,
|
|
116
|
+
contractSessionFrom,
|
|
110
117
|
filterChangedGovernedFiles,
|
|
111
118
|
runTeamPreflight,
|
|
112
119
|
ungovernedDumpMessage,
|
|
@@ -120,6 +127,16 @@ import {
|
|
|
120
127
|
} from './lib/project-root.mjs';
|
|
121
128
|
import { demoteArkRuleTeethUnderClassificationFloor } from './lib/rules-under-contract.mjs';
|
|
122
129
|
|
|
130
|
+
function resolveDesignDeltaBaseRef(root, explicit, env = process.env) {
|
|
131
|
+
const flag = typeof explicit === 'string' ? explicit.trim() : '';
|
|
132
|
+
if (flag) return flag;
|
|
133
|
+
const envRef = normalizePolicyBaseRef(env.ARK_POLICY_BASE_REF);
|
|
134
|
+
if (envRef) return envRef;
|
|
135
|
+
const githubBase = typeof env.GITHUB_BASE_REF === 'string' ? env.GITHUB_BASE_REF.trim() : '';
|
|
136
|
+
if (githubBase) return `origin/${githubBase}`;
|
|
137
|
+
return discoverLocalBaseRef(root) || undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
123
140
|
function parseArgs(argv) {
|
|
124
141
|
const args = {
|
|
125
142
|
root: process.cwd(),
|
|
@@ -573,6 +590,60 @@ function runSuggestInclude(args) {
|
|
|
573
590
|
console.log(color.dim(payload.note));
|
|
574
591
|
}
|
|
575
592
|
|
|
593
|
+
const MATURE_LAYER_RULES = Object.freeze([
|
|
594
|
+
{ from: 'SharedKernel', to: 'DomainModel', allowed: false },
|
|
595
|
+
{ from: 'SharedKernel', to: 'ApplicationOrchestration', allowed: false },
|
|
596
|
+
{ from: 'SharedKernel', to: 'PresentationAdapters', allowed: false },
|
|
597
|
+
{ from: 'SharedKernel', to: 'PersistenceAdapters', allowed: false },
|
|
598
|
+
{ from: 'SharedKernel', to: 'CompositionRoot', allowed: false },
|
|
599
|
+
{ from: 'DomainModel', to: 'CompositionRoot', allowed: false },
|
|
600
|
+
{ from: 'PresentationAdapters', to: 'CompositionRoot', allowed: false },
|
|
601
|
+
{ from: 'PersistenceAdapters', to: 'CompositionRoot', allowed: false },
|
|
602
|
+
]);
|
|
603
|
+
|
|
604
|
+
function ensureMatureAdoptionLayers(layers, byLayer) {
|
|
605
|
+
const names = new Set(layers.map((layer) => layer.name));
|
|
606
|
+
const next = [...layers];
|
|
607
|
+
if (!names.has('SharedKernel')) {
|
|
608
|
+
next.push({
|
|
609
|
+
name: 'SharedKernel',
|
|
610
|
+
patterns: [...(byLayer.get('SharedKernel') ?? SHARED_KERNEL_PATH_PATTERNS)],
|
|
611
|
+
reserved: true,
|
|
612
|
+
allowEmpty: true,
|
|
613
|
+
description: 'Shared types and constants. Persistence and Presentation may import this; it imports nothing else.',
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
if (!names.has('CompositionRoot')) {
|
|
617
|
+
next.push({
|
|
618
|
+
name: 'CompositionRoot',
|
|
619
|
+
patterns: [...(byLayer.get('CompositionRoot') ?? COMPOSITION_ROOT_PATH_PATTERNS)],
|
|
620
|
+
reserved: true,
|
|
621
|
+
allowEmpty: true,
|
|
622
|
+
description: 'DI / bootstrap wiring. May import Domain and Persistence; Domain must not import this.',
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
return next;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function writeAdoptGoldenPattern(root) {
|
|
629
|
+
const dir = path.join(root, '.ark');
|
|
630
|
+
const dest = path.join(dir, 'golden-pattern.json');
|
|
631
|
+
if (fs.existsSync(dest)) return { wrote: false, path: '.ark/golden-pattern.json' };
|
|
632
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
633
|
+
const golden = {
|
|
634
|
+
schemaVersion: '1',
|
|
635
|
+
name: 'feature-folders',
|
|
636
|
+
norm:
|
|
637
|
+
'New modules live under src/<feature>/{domain,application,composition,infrastructure}. ' +
|
|
638
|
+
'Types and constants go to SharedKernel. Wiring goes to CompositionRoot. ' +
|
|
639
|
+
'Presentation never imports Domain. Persistence never imports Application.',
|
|
640
|
+
newCodeHome: 'src',
|
|
641
|
+
examplePath: 'src/example/domain/model.ts',
|
|
642
|
+
};
|
|
643
|
+
fs.writeFileSync(dest, `${JSON.stringify(golden, null, 2)}\n`);
|
|
644
|
+
return { wrote: true, path: '.ark/golden-pattern.json' };
|
|
645
|
+
}
|
|
646
|
+
|
|
576
647
|
/**
|
|
577
648
|
* Contract-adopt: expand include + layer patterns from ungoverned proposals.
|
|
578
649
|
* Read-only unless --write. Does not weaken rules or baseline violations.
|
|
@@ -632,7 +703,9 @@ function runAdoptContract(args) {
|
|
|
632
703
|
...APPLICATION_LIB_ORCHESTRATION_PATTERNS,
|
|
633
704
|
'api/**',
|
|
634
705
|
];
|
|
635
|
-
const domainPatterns = [...DOMAIN_PATH_PATTERNS];
|
|
706
|
+
const domainPatterns = [...DOMAIN_PATH_PATTERNS, 'src/**/domain/**'];
|
|
707
|
+
const sharedKernelPatterns = [...SHARED_KERNEL_PATH_PATTERNS];
|
|
708
|
+
const compositionRootPatterns = [...COMPOSITION_ROOT_PATH_PATTERNS];
|
|
636
709
|
|
|
637
710
|
// Build pattern additions from unclassified suggestions (path-aware).
|
|
638
711
|
const byLayer = new Map([
|
|
@@ -640,33 +713,45 @@ function runAdoptContract(args) {
|
|
|
640
713
|
['PersistenceAdapters', [...persistencePatterns]],
|
|
641
714
|
['ApplicationOrchestration', [...applicationPatterns]],
|
|
642
715
|
['DomainModel', [...domainPatterns]],
|
|
716
|
+
['SharedKernel', [...sharedKernelPatterns]],
|
|
717
|
+
['CompositionRoot', [...compositionRootPatterns]],
|
|
643
718
|
]);
|
|
644
719
|
for (const suggestion of cov.suggestions ?? []) {
|
|
645
720
|
if (suggestion.unrecognized || !suggestion.layer) continue;
|
|
646
|
-
|
|
721
|
+
const dir = String(suggestion.dir || '');
|
|
722
|
+
// Never dump bare lib/ into Presentation or Application.
|
|
723
|
+
if (dir === 'lib' || dir === 'src/lib' || dir.endsWith('/lib')) {
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
647
726
|
if (
|
|
648
727
|
suggestion.layer === 'PresentationAdapters' &&
|
|
649
|
-
(
|
|
728
|
+
(dir === 'lib' || dir.endsWith('/lib'))
|
|
650
729
|
) {
|
|
651
730
|
continue;
|
|
652
731
|
}
|
|
653
|
-
|
|
732
|
+
let layerName = suggestion.layer;
|
|
733
|
+
if (/(^|\/)(types|constants|shared)(\/|$)/i.test(dir)) layerName = 'SharedKernel';
|
|
734
|
+
else if (/(^|\/)(composition|factories|bootstrap)(\/|$)/i.test(dir)) layerName = 'CompositionRoot';
|
|
735
|
+
else if (/(^|\/)domain(\/|$)/i.test(dir)) layerName = 'DomainModel';
|
|
736
|
+
const list = byLayer.get(layerName) ?? [];
|
|
654
737
|
const glob = suggestion.dir === '.' ? null : `${suggestion.dir}/**`;
|
|
655
|
-
if (glob && !list.includes(glob)
|
|
656
|
-
|
|
738
|
+
if (glob && !list.includes(glob) && glob !== 'src/lib/**' && glob !== 'lib/**') {
|
|
739
|
+
list.push(glob);
|
|
740
|
+
}
|
|
741
|
+
byLayer.set(layerName, list);
|
|
657
742
|
}
|
|
658
743
|
|
|
659
|
-
const
|
|
744
|
+
const stripLibVacuum = (patterns) =>
|
|
745
|
+
(patterns || []).filter((p) => p !== '**/lib/**' && p !== 'lib/**' && p !== 'src/lib/**');
|
|
746
|
+
|
|
747
|
+
let layers = (config.layers || []).map((layer) => {
|
|
660
748
|
const extras = byLayer.get(layer.name);
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const cleaned = (layer.patterns || []).filter((p) => {
|
|
664
|
-
if (layer.name !== 'PresentationAdapters') return true;
|
|
665
|
-
return p !== '**/lib/**' && p !== 'lib/**' && p !== 'src/lib/**';
|
|
666
|
-
});
|
|
749
|
+
const cleaned = stripLibVacuum(layer.patterns || []);
|
|
750
|
+
if (!extras?.length) return { ...layer, patterns: cleaned };
|
|
667
751
|
const patterns = [...new Set([...cleaned, ...extras])];
|
|
668
752
|
return { ...layer, patterns };
|
|
669
753
|
});
|
|
754
|
+
layers = ensureMatureAdoptionLayers(layers, byLayer);
|
|
670
755
|
// If no PresentationAdapters layer, leave layers as-is (don't invent full profile).
|
|
671
756
|
const proposal = {
|
|
672
757
|
ok: true,
|
|
@@ -681,6 +766,10 @@ function runAdoptContract(args) {
|
|
|
681
766
|
persistencePatterns,
|
|
682
767
|
applicationPatterns,
|
|
683
768
|
domainPatterns,
|
|
769
|
+
sharedKernelPatterns,
|
|
770
|
+
compositionRootPatterns,
|
|
771
|
+
proposedLayers: ['SharedKernel', 'CompositionRoot', 'DomainModel'],
|
|
772
|
+
goldenPattern: '.ark/golden-pattern.json',
|
|
684
773
|
totalFiles: cov.totalFiles,
|
|
685
774
|
governedPercent: cov.governed.percent,
|
|
686
775
|
unclassified: cov.unclassified.count,
|
|
@@ -698,12 +787,21 @@ function runAdoptContract(args) {
|
|
|
698
787
|
}
|
|
699
788
|
|
|
700
789
|
if (args.write) {
|
|
790
|
+
const existingRules = Array.isArray(config.rules) ? config.rules : [];
|
|
791
|
+
const rules = [...existingRules];
|
|
792
|
+
for (const rule of MATURE_LAYER_RULES) {
|
|
793
|
+
if (!rules.some((r) => r.from === rule.from && r.to === rule.to && r.allowed === rule.allowed)) {
|
|
794
|
+
rules.push(rule);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
701
797
|
const next = {
|
|
702
798
|
...config,
|
|
703
799
|
include: proposal.after.include,
|
|
704
800
|
layers,
|
|
801
|
+
rules,
|
|
705
802
|
};
|
|
706
803
|
fs.writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`);
|
|
804
|
+
proposal.golden = writeAdoptGoldenPattern(root);
|
|
707
805
|
proposal.wrote = true;
|
|
708
806
|
}
|
|
709
807
|
|
|
@@ -720,7 +818,8 @@ function runAdoptContract(args) {
|
|
|
720
818
|
);
|
|
721
819
|
console.log(` presentation patterns += ${uiPatterns.join(', ')}`);
|
|
722
820
|
console.log(` persistence patterns += (data clients / db / auth — never bare lib→Presentation)`);
|
|
723
|
-
console.log(` application patterns += Next/Vercel API shells`);
|
|
821
|
+
console.log(` application patterns += Next/Vercel API shells (never bare src/lib/**)`);
|
|
822
|
+
console.log(` shared kernel += types/constants; composition root += wiring`);
|
|
724
823
|
if (proposal.wrote) {
|
|
725
824
|
console.log(color.green(` wrote ${path.relative(root, configPath) || args.config}`));
|
|
726
825
|
console.log(color.dim(` Next: ${arkCommand(root, 'ark-check', '--coverage')} then --plan`));
|
|
@@ -1240,6 +1339,11 @@ async function main() {
|
|
|
1240
1339
|
console.log('✔ Ark check passed (no governed source or constitution files in the diff).');
|
|
1241
1340
|
} else {
|
|
1242
1341
|
console.error(preflight.halt.message);
|
|
1342
|
+
const blocking = policyDelta?.findings?.find(
|
|
1343
|
+
(finding) =>
|
|
1344
|
+
finding.classification === 'weakening' || finding.classification === 'judgment-required'
|
|
1345
|
+
);
|
|
1346
|
+
if (blocking?.nextAction) console.error(`Next: ${blocking.nextAction}`);
|
|
1243
1347
|
}
|
|
1244
1348
|
process.exitCode = preflight.halt.exitCode;
|
|
1245
1349
|
return;
|
|
@@ -1322,7 +1426,17 @@ async function main() {
|
|
|
1322
1426
|
populatedLayerCount,
|
|
1323
1427
|
});
|
|
1324
1428
|
|
|
1325
|
-
const
|
|
1429
|
+
const createdPathsOnly = Boolean(args.strictMerge && !args.failOnNewSmells);
|
|
1430
|
+
const designCheck = createDesignDeltaCheck({
|
|
1431
|
+
enabled: args.failOnNewSmells || args.strictMerge,
|
|
1432
|
+
createdPathsOnly,
|
|
1433
|
+
missingBase: createdPathsOnly ? 'skip' : 'fail-closed',
|
|
1434
|
+
root,
|
|
1435
|
+
config,
|
|
1436
|
+
configPath: args.config,
|
|
1437
|
+
baseRef: resolveDesignDeltaBaseRef(root, args.baseRef),
|
|
1438
|
+
ts,
|
|
1439
|
+
});
|
|
1326
1440
|
const designDelta = designCheck.result;
|
|
1327
1441
|
|
|
1328
1442
|
if (args.doctor) {
|
|
@@ -1333,6 +1447,7 @@ async function main() {
|
|
|
1333
1447
|
configWalkedUp: args.configWalkedUp === true,
|
|
1334
1448
|
safety, designDelta,
|
|
1335
1449
|
ts, parseHealth, completeness,
|
|
1450
|
+
all: args.all === true,
|
|
1336
1451
|
});
|
|
1337
1452
|
if (designDelta) process.exitCode = !designDelta.complete ? 2 : designDelta.valid ? 0 : 1; return;
|
|
1338
1453
|
}
|
|
@@ -1343,6 +1458,11 @@ async function main() {
|
|
|
1343
1458
|
}
|
|
1344
1459
|
|
|
1345
1460
|
if (args.updateBaseline) {
|
|
1461
|
+
if (!contractSessionFrom(args)) {
|
|
1462
|
+
console.error('Growing the baseline requires --contract-session. Freeze in a law-only PR.');
|
|
1463
|
+
process.exitCode = 1;
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1346
1466
|
const summary = summarizeViolations(violations);
|
|
1347
1467
|
// Bloquear y avisar: a lopsided freeze buries a likely contract bug as "debt". Refuse it
|
|
1348
1468
|
// (unless --force), diagnose, and point at the contract fix instead of the baseline.
|
package/bin/ark-mcp-runtime.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
detectWorkspaces,
|
|
22
22
|
detectTsPackageRoots,
|
|
23
23
|
resolveIncludeRoots,
|
|
24
|
+
isScanExcludedRelative,
|
|
24
25
|
} from './ark-shared.mjs';
|
|
25
26
|
import { effectiveCapabilityDeny, stableSerialize } from './lib/analysis-engine.mjs';
|
|
26
27
|
import { createImportTargetResolver } from './lib/import-resolve.mjs';
|
|
@@ -866,6 +867,11 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
866
867
|
emitHostAllow(output, { antigravityStyle, cursorStyle });
|
|
867
868
|
return;
|
|
868
869
|
}
|
|
870
|
+
const normalizedRel = rel.split(path.sep).join('/');
|
|
871
|
+
if (isScanExcludedRelative(normalizedRel, config)) {
|
|
872
|
+
emitHostAllow(output, { antigravityStyle, cursorStyle });
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
869
875
|
|
|
870
876
|
const source = proposedSource(toolName, toolInput);
|
|
871
877
|
if (typeof source !== 'string') {
|
|
@@ -897,7 +903,6 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
897
903
|
autoPatch: null,
|
|
898
904
|
};
|
|
899
905
|
})();
|
|
900
|
-
const normalizedRel = rel.split(path.sep).join('/');
|
|
901
906
|
const designDelta = args.failOnNewSmells && layer
|
|
902
907
|
? evaluateWriteDesignDelta({
|
|
903
908
|
root: args.root,
|
|
@@ -930,7 +935,10 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
930
935
|
// New file: nothing pre-exists, every violation is new.
|
|
931
936
|
}
|
|
932
937
|
const newViolations = (result.violations ?? []).filter((violation) => {
|
|
933
|
-
|
|
938
|
+
const rule = String(violation.ruleId ?? violation.code);
|
|
939
|
+
// Incremental mid-edit parse errors are normal for agents — do not deny solely on them.
|
|
940
|
+
if (rule === 'ANALYSIS_PARSE_INCOMPLETE') return false;
|
|
941
|
+
if (rule.startsWith('ANALYSIS_')) return true;
|
|
934
942
|
const key = violationKey(violation);
|
|
935
943
|
const remaining = existingCounts.get(key) ?? 0;
|
|
936
944
|
if (remaining === 0) return true;
|
|
@@ -2176,11 +2184,10 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2176
2184
|
{
|
|
2177
2185
|
name: 'ark_place',
|
|
2178
2186
|
description:
|
|
2179
|
-
'Place a file in the architecture:
|
|
2180
|
-
'Returns layer, mayImport / mustNotImport, forbiddenGlobals, and
|
|
2181
|
-
'(
|
|
2187
|
+
'Place a file in the architecture: filePath is required (fail-closed without it — never invents components/*.tsx or defaults to Presentation). ' +
|
|
2188
|
+
'Returns layer, mayImport / mustNotImport, forbiddenGlobals, and goldenPattern ' +
|
|
2189
|
+
'(load-bearing for NEW code when .ark/golden-pattern.json exists — adopt generates it). ' +
|
|
2182
2190
|
'Call BEFORE writing a new file. ' +
|
|
2183
|
-
'If only description is given, returns a conventional path proposal under a governed layer. ' +
|
|
2184
2191
|
'Prefer ark_prepare_write when you already have the source snippet (place+validate+autoPatch in one call).',
|
|
2185
2192
|
inputSchema: {
|
|
2186
2193
|
type: 'object',
|
|
@@ -2192,7 +2199,7 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2192
2199
|
description: {
|
|
2193
2200
|
type: 'string',
|
|
2194
2201
|
description:
|
|
2195
|
-
'What you are building
|
|
2202
|
+
'What you are building. Does not invent a path — pass filePath. Without filePath the tool fail-closes.',
|
|
2196
2203
|
},
|
|
2197
2204
|
},
|
|
2198
2205
|
},
|
|
@@ -2641,31 +2648,12 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2641
2648
|
const golden = loadGoldenPattern(args.root);
|
|
2642
2649
|
const withGolden = (placement) => attachGoldenToPlacement(placement, golden);
|
|
2643
2650
|
|
|
2644
|
-
if (
|
|
2645
|
-
const slug = description
|
|
2646
|
-
.trim()
|
|
2647
|
-
.toLowerCase()
|
|
2648
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
2649
|
-
.replace(/^-|-$/g, '')
|
|
2650
|
-
.slice(0, 48) || 'component';
|
|
2651
|
-
const proposedPath = `src/components/${slug}.tsx`;
|
|
2652
|
-
const layerName = inferLayer(proposedPath, config, args.root) || 'PresentationAdapters';
|
|
2653
|
-
return withGolden({
|
|
2654
|
-
filePath: proposedPath,
|
|
2655
|
-
proposed: true,
|
|
2656
|
-
description: description.trim(),
|
|
2657
|
-
layer: layerName,
|
|
2658
|
-
governed: Boolean(inferLayer(proposedPath, config, args.root)),
|
|
2659
|
-
note:
|
|
2660
|
-
'filePath was omitted — proposed a conventional path from description. ' +
|
|
2661
|
-
'Pass filePath explicitly for authoritative placement.',
|
|
2662
|
-
});
|
|
2663
|
-
}
|
|
2664
|
-
if (typeof filePath !== 'string' || !filePath) {
|
|
2651
|
+
if (typeof filePath !== 'string' || !filePath.trim()) {
|
|
2665
2652
|
return {
|
|
2666
2653
|
error:
|
|
2667
|
-
'
|
|
2668
|
-
'Example: { "filePath": "src/
|
|
2654
|
+
'ark_place requires filePath. Fail-closed: will not invent a path (never default to Presentation or components/*.tsx). ' +
|
|
2655
|
+
'Example: { "filePath": "src/lib/repositories/orders-repository.ts" }.',
|
|
2656
|
+
failClosed: true,
|
|
2669
2657
|
};
|
|
2670
2658
|
}
|
|
2671
2659
|
const layerName = inferLayer(filePath, config, args.root);
|
package/bin/ark.mjs
CHANGED
|
@@ -694,11 +694,13 @@ async function start(args) {
|
|
|
694
694
|
// Modes are detected (Suggest/Adapt/Enforce), not user-picked settings.
|
|
695
695
|
// Soft-block false-green using the same doctor adoption gap (no second detector).
|
|
696
696
|
let falseGreenGap = null;
|
|
697
|
+
let adopted = null;
|
|
697
698
|
try {
|
|
698
699
|
const doc = JSON.parse(doctorCapture.stdout || '{}');
|
|
699
700
|
falseGreenGap = (doc.doctor?.adoption?.gaps ?? []).find(
|
|
700
701
|
(g) => g?.id === FALSE_GREEN_GAP_ID
|
|
701
702
|
);
|
|
703
|
+
adopted = doc.doctor?.adoptionStance ?? null;
|
|
702
704
|
} catch {
|
|
703
705
|
falseGreenGap = null;
|
|
704
706
|
}
|
|
@@ -714,9 +716,17 @@ async function start(args) {
|
|
|
714
716
|
console.log(` • ${falseGreenGap.message}`);
|
|
715
717
|
console.log(` • Next: ${falseGreenGap.fix}`);
|
|
716
718
|
} else if (mode === 'enforce' && planOk) {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
719
|
+
if (adopted === 'required-merge' || adopted === 'advisory-only-acked') {
|
|
720
|
+
console.log('Done — status: ENFORCE (gates can honestly protect you).');
|
|
721
|
+
console.log('What happens now:');
|
|
722
|
+
console.log(' • Every edit is checked (in CI and, if wired, at write time).');
|
|
723
|
+
} else {
|
|
724
|
+
console.log('Done — status: ENFORCE (contract edges clean; merge boundary not adopted).');
|
|
725
|
+
console.log('What happens now:');
|
|
726
|
+
console.log(
|
|
727
|
+
' • CI workflow written; make the GitHub status required on arkgate-check --strict-merge, or write .ark/adoption-stance.json with stance: "advisory-only".'
|
|
728
|
+
);
|
|
729
|
+
}
|
|
720
730
|
} else if (mode === 'suggest') {
|
|
721
731
|
console.log('Done — status: SUGGEST (starting shape installed; expand as you grow).');
|
|
722
732
|
console.log('What happens now:');
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* Pure CLI helper (bin/lib/adapter-contract.mjs). Zero Node I/O.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { layerImportNextAction } from './remediation.mjs';
|
|
12
|
+
/** Versioned public result contract shared by every ArkGate enforcement adapter. */
|
|
11
13
|
/**
|
|
12
14
|
* 1.5 adds stable finding refs on every factory-emitted diagnostic (ACS06):
|
|
13
15
|
* `findingRef`, `targetKey` (baseline-compatible), `docsCodePath`.
|
|
@@ -76,15 +78,17 @@ export function adapterDocsCodePath(ruleId) {
|
|
|
76
78
|
}
|
|
77
79
|
function nextActionForDiagnostic(ruleId, evidence, violation) {
|
|
78
80
|
if (ruleId === 'LAYER_IMPORT_VIOLATION') {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
return layerImportNextAction({
|
|
82
|
+
ruleId,
|
|
83
|
+
typeOnly: evidence.typeOnly === true,
|
|
84
|
+
targetTypeOnlyExports: violation.targetTypeOnlyExports === true,
|
|
85
|
+
namedBindingsTypeOnly: violation.namedBindingsTypeOnly === true,
|
|
86
|
+
peerIsolation: violation.peerIsolation === true,
|
|
87
|
+
portProofEligible: violation.portProofEligible === true,
|
|
88
|
+
fromLayer: text(evidence.fromLayer) ?? undefined,
|
|
89
|
+
toLayer: text(evidence.toLayer) ?? undefined,
|
|
90
|
+
target: text(evidence.target) ?? text(violation.target) ?? undefined,
|
|
91
|
+
});
|
|
88
92
|
}
|
|
89
93
|
if (ruleId === 'FORBIDDEN_GLOBAL') {
|
|
90
94
|
return `Inject ${evidence.target ?? 'the capability'} through a port, test at the public interface, then preflight again.`;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D0 adoption stance — required merge status or explicit advisory-only ack.
|
|
3
|
+
* Tooling I/O. Never invents GitHub required from workflow YAML presence.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
export const ADOPTION_STANCE_REL = '.ark/adoption-stance.json';
|
|
9
|
+
export const ADOPTION_STANCE_VALUE = 'advisory-only';
|
|
10
|
+
export const ADOPTED_REQUIRED_MERGE = 'required-merge';
|
|
11
|
+
export const ADOPTED_ADVISORY_ACKED = 'advisory-only-acked';
|
|
12
|
+
export const ADOPTED_NOT = 'not-adopted';
|
|
13
|
+
|
|
14
|
+
export const NOT_ADOPTED_NEXT_ACTION =
|
|
15
|
+
'Make arkgate-check --strict-merge a required GitHub status, or write .ark/adoption-stance.json with stance: "advisory-only"';
|
|
16
|
+
|
|
17
|
+
export const MERGE_BOUNDARY_NOT_REQUIRED = 'merge-boundary-not-required';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} root
|
|
21
|
+
* @returns {{ schemaVersion?: string, stance?: string, ackedAt?: string, reason?: string } | null}
|
|
22
|
+
*/
|
|
23
|
+
export function readAdoptionStance(root) {
|
|
24
|
+
const file = path.join(root, ADOPTION_STANCE_REL);
|
|
25
|
+
if (!fs.existsSync(file)) return null;
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
28
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stanceValue(stance) {
|
|
35
|
+
if (typeof stance === 'string') return stance;
|
|
36
|
+
if (stance && typeof stance === 'object' && typeof stance.stance === 'string') {
|
|
37
|
+
return stance.stance;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Closed enum: required-merge | advisory-only-acked | not-adopted.
|
|
44
|
+
* Workflow presence is never enough.
|
|
45
|
+
*
|
|
46
|
+
* @param {{
|
|
47
|
+
* stance?: { stance?: string } | string | null,
|
|
48
|
+
* github?: { arkCheckRequired?: unknown, requiredStatusConfigured?: unknown },
|
|
49
|
+
* ci?: { state?: string, requiredStatusConfigured?: unknown },
|
|
50
|
+
* }} [input]
|
|
51
|
+
*/
|
|
52
|
+
export function classifyAdopted(input = {}) {
|
|
53
|
+
const github = input.github && typeof input.github === 'object' ? input.github : {};
|
|
54
|
+
const ci = input.ci && typeof input.ci === 'object' ? input.ci : {};
|
|
55
|
+
const required =
|
|
56
|
+
github.arkCheckRequired === true ||
|
|
57
|
+
github.requiredStatusConfigured === true ||
|
|
58
|
+
ci.requiredStatusConfigured === true ||
|
|
59
|
+
ci.state === 'required';
|
|
60
|
+
if (required) return ADOPTED_REQUIRED_MERGE;
|
|
61
|
+
if (stanceValue(input.stance) === ADOPTION_STANCE_VALUE) return ADOPTED_ADVISORY_ACKED;
|
|
62
|
+
return ADOPTED_NOT;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isAdopted(kind) {
|
|
66
|
+
return kind === ADOPTED_REQUIRED_MERGE || kind === ADOPTED_ADVISORY_ACKED;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Map doctor adoption + writePath into the ci-merge-boundary github input.
|
|
71
|
+
* Never sets requiredStatusConfigured false from a missing GitHub query.
|
|
72
|
+
*
|
|
73
|
+
* @param {object} [adoption]
|
|
74
|
+
* @param {object} [writePath]
|
|
75
|
+
*/
|
|
76
|
+
export function githubEvidenceForCiMergeBoundary(adoption, writePath) {
|
|
77
|
+
const github =
|
|
78
|
+
adoption?.enforcement?.github && typeof adoption.enforcement.github === 'object'
|
|
79
|
+
? adoption.enforcement.github
|
|
80
|
+
: {};
|
|
81
|
+
const ci =
|
|
82
|
+
adoption?.enforcement?.ci && typeof adoption.enforcement.ci === 'object'
|
|
83
|
+
? adoption.enforcement.ci
|
|
84
|
+
: {};
|
|
85
|
+
const ciMerge =
|
|
86
|
+
writePath?.enforcementState?.ciMerge && typeof writePath.enforcementState.ciMerge === 'object'
|
|
87
|
+
? writePath.enforcementState.ciMerge
|
|
88
|
+
: {};
|
|
89
|
+
const required =
|
|
90
|
+
github.arkCheckRequired === true ||
|
|
91
|
+
github.requiredStatusConfigured === true ||
|
|
92
|
+
ciMerge.required === true;
|
|
93
|
+
return {
|
|
94
|
+
...github,
|
|
95
|
+
...(required ? { arkCheckRequired: true, requiredStatusConfigured: true } : {}),
|
|
96
|
+
workflowPresent:
|
|
97
|
+
github.workflowPresent === true ||
|
|
98
|
+
ci.hasArkCheckWorkflow === true ||
|
|
99
|
+
writePath?.capabilities?.['merge-gate'] === true ||
|
|
100
|
+
writePath?.inventory?.capabilities?.['merge-gate'] === true,
|
|
101
|
+
plan: github.plan,
|
|
102
|
+
canRequire: github.canRequire,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -261,6 +261,14 @@ export function runAgentProjectionCommand(args = {}) {
|
|
|
261
261
|
profile: args.profile,
|
|
262
262
|
arkgateVersion: args.arkgateVersion,
|
|
263
263
|
});
|
|
264
|
+
const claudePlan = planAgentProjectionRefresh({
|
|
265
|
+
root: args.root,
|
|
266
|
+
config: args.config,
|
|
267
|
+
host: args.host || 'claude',
|
|
268
|
+
profile: args.profile,
|
|
269
|
+
arkgateVersion: args.arkgateVersion,
|
|
270
|
+
targetRelativePath: 'CLAUDE.md',
|
|
271
|
+
});
|
|
264
272
|
|
|
265
273
|
if (stdoutOnly) {
|
|
266
274
|
if (asJson) {
|
|
@@ -337,8 +345,10 @@ export function runAgentProjectionCommand(args = {}) {
|
|
|
337
345
|
}
|
|
338
346
|
|
|
339
347
|
let applyResult = { wrote: false, action: plan.action, path: plan.path };
|
|
348
|
+
let claudeApply = { wrote: false, action: claudePlan.action, path: claudePlan.path };
|
|
340
349
|
if (doWrite) {
|
|
341
350
|
applyResult = applyAgentProjectionRefresh(plan, { write: true });
|
|
351
|
+
claudeApply = applyAgentProjectionRefresh(claudePlan, { write: true });
|
|
342
352
|
}
|
|
343
353
|
|
|
344
354
|
if (asJson) {
|
|
@@ -349,6 +359,14 @@ export function runAgentProjectionCommand(args = {}) {
|
|
|
349
359
|
arkgateVersion: plan.packageVersion,
|
|
350
360
|
nonAuthoritative: true,
|
|
351
361
|
path: plan.relativePath,
|
|
362
|
+
hosts: {
|
|
363
|
+
agentsMd: { path: plan.relativePath, wrote: applyResult.wrote, action: applyResult.action },
|
|
364
|
+
claudeMd: {
|
|
365
|
+
path: claudePlan.relativePath,
|
|
366
|
+
wrote: claudeApply.wrote,
|
|
367
|
+
action: claudeApply.action,
|
|
368
|
+
},
|
|
369
|
+
},
|
|
352
370
|
action: applyResult.action,
|
|
353
371
|
wrote: applyResult.wrote,
|
|
354
372
|
wouldWrite: plan.wouldWrite,
|
|
@@ -148,10 +148,10 @@ export function buildAgentProjectionBody(facts) {
|
|
|
148
148
|
'',
|
|
149
149
|
];
|
|
150
150
|
if (profile === 'compact') {
|
|
151
|
-
lines.push('### Primary path', '', '1. Run doctor (`ark-check --doctor`) — status light + primary next action.', '2. Read the improvement compass (not a score). Name leftover work in plain language; never “done” on green imports alone while leftover design work remains.', '3.
|
|
151
|
+
lines.push('### Primary path', '', '1. Run doctor (`ark-check --doctor`) — status light + primary next action. Prefer the project-local CLI; do not wait on MCP “still connecting”.', '2. Read the improvement compass (not a score). Name leftover work in plain language; never “done” on green imports alone while leftover design work remains.', '3. Identity handshake is optional when the CLI already resolved the project root. Call `ark_identity` only when using MCP evidence.', '4. Read architecture config with `ark_manifest` (same expectation) or the local `ark.config.json`. `ark://manifest` is compatibility-only / unverified.', '5. Place files inside configured layers; validate; run the check command above on violations — fix architecture, do not weaken the gate.', '6. Single door: import-rule debt → fix; leftover design work / residual shape lenses → map then guided apply with user OK.', '', '### Contract layers (summary)', '', formatAgentProjectionLayers(layers), '');
|
|
152
152
|
}
|
|
153
153
|
else {
|
|
154
|
-
lines.push('### Contract layers', '', formatAgentProjectionLayers(layers), '', 'When creating a **new** kind of code that no layer covers, update `ark.config.json` first (`/ark-adopt`), then place the file.', '', '### Diagnostic codes (short list)', '', formatAgentProjectionCatalogShortList(catalog, docsPath), '', '### Session truth', '', '- Machine snapshot: `ark status --json` (or MCP `ark_status`) — identity, activation honesty, last check, residual counts. **Not a score.**', '- Authoritative contract: `ark_manifest` after a matched `ark_identity` handshake.', '');
|
|
154
|
+
lines.push('### Contract layers', '', formatAgentProjectionLayers(layers), '', 'When creating a **new** kind of code that no layer covers, update `ark.config.json` first (`/ark-adopt`), then place the file.', '', '### Diagnostic codes (short list)', '', formatAgentProjectionCatalogShortList(catalog, docsPath), '', '### Session truth', '', '- Machine snapshot: `ark status --json` (or MCP `ark_status`) — identity, activation honesty, last check, residual counts. **Not a score.**', '- Authoritative contract: local `ark.config.json` / CLI, or `ark_manifest` after a matched `ark_identity` handshake. Identity is optional when CLI already resolved the root.', '- Host docs: the same projection schema is merged into `AGENTS.md` and `CLAUDE.md` (`ark agents-md --write`).', '');
|
|
155
155
|
}
|
|
156
156
|
lines.push('### Enforcement surfaces (authoritative)', '', AGENT_PROJECTION_ENFORCEMENT_SURFACES.map((surface) => `- \`${surface}\``).join('\n'), '');
|
|
157
157
|
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
|