arkgate 4.8.13 → 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 +219 -4
- package/README.md +33 -22
- package/SECURITY.md +5 -3
- package/bin/ark-check-runtime.mjs +25 -57
- 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-projection-formatters.mjs +2 -0
- package/bin/lib/agent-skills-package.mjs +241 -8
- package/bin/lib/analysis-completeness.mjs +38 -2
- package/bin/lib/analysis-engine.mjs +6 -6
- package/bin/lib/architecture-scan.mjs +33 -5
- package/bin/lib/ark-order-doctor.mjs +7 -1
- package/bin/lib/ark-order-report.mjs +2 -1
- 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/ci-and-commands.mjs +7 -2
- package/bin/lib/config-contract.mjs +64 -1
- package/bin/lib/design-smells.mjs +21 -1
- package/bin/lib/diagnostic-catalog.mjs +5 -3
- package/bin/lib/doctor-advisories.mjs +109 -17
- package/bin/lib/doctor-human.mjs +20 -6
- 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 +19 -3
- package/bin/lib/gate-files.mjs +40 -3
- 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/install-migrate.mjs +23 -0
- package/bin/lib/layer-description.mjs +90 -8
- package/bin/lib/mcp-hook-payload.mjs +57 -1
- package/bin/lib/no-domain-frontend.mjs +91 -0
- package/bin/lib/presets.mjs +3 -4
- package/bin/lib/product-copy.mjs +19 -0
- package/bin/lib/projected-governed-coverage.mjs +114 -0
- package/bin/lib/recommend-cli.mjs +54 -0
- package/bin/lib/remediation.mjs +6 -2
- package/bin/lib/resolved-candidate-facts.mjs +225 -100
- package/bin/lib/rules-under-contract.mjs +117 -2
- package/bin/lib/scan-files.mjs +39 -0
- package/bin/lib/snippet-analysis.mjs +14 -8
- package/bin/lib/start-preview.mjs +8 -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/upgrade-whats-new.mjs +3 -3
- package/bin/lib/violations.mjs +43 -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-DA565Lja.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 +126 -19
- 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 +8 -6
- package/docs/agent-guide.md +35 -17
- package/docs/ai-gates.md +13 -4
- package/docs/arkorder.md +11 -4
- package/docs/brownfield-adoption.md +4 -1
- package/docs/configuration.md +63 -14
- package/docs/develop.md +4 -1
- package/docs/diagnostics.md +30 -10
- package/docs/enthusiast/README.md +6 -1
- package/docs/enthusiast/how-to-agent-gates.md +5 -0
- package/docs/enthusiast/how-to-gallery-starter.md +2 -1
- package/docs/enthusiast/how-to-pick-shape.md +1 -1
- package/docs/package-surface.md +10 -6
- package/docs/product-voice.md +32 -6
- package/docs/threat-model.md +2 -2
- package/docs/typescript-support.md +3 -3
- package/docs/use.md +23 -11
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +23 -2
- package/server.json +2 -2
- package/templates/agent-skills/README.md +7 -4
- package/templates/agent-skills/ark-adopt/SKILL.md +91 -6
- package/templates/agent-skills/ark-architect/SKILL.md +5 -18
- package/templates/agent-skills/ark-autopilot/SKILL.md +14 -6
- package/templates/agent-skills/ark-contract/SKILL.md +9 -20
- package/templates/agent-skills/ark-coverage/SKILL.md +13 -9
- package/templates/agent-skills/ark-explain/SKILL.md +11 -5
- package/templates/agent-skills/ark-explore/SKILL.md +39 -5
- package/templates/agent-skills/ark-fix/SKILL.md +15 -20
- package/templates/agent-skills/ark-loop/SKILL.md +14 -20
- package/templates/agent-skills/ark-order/SKILL.md +200 -0
- package/templates/agent-skills/ark-place/SKILL.md +49 -11
- package/templates/agent-skills/ark-runtime/SKILL.md +21 -6
- package/templates/agent-skills/ark-think/SKILL.md +24 -126
- package/templates/agent-skills/ark-upgrade/SKILL.md +14 -3
- package/templates/arkrules/DomainModel.json +14 -1
- package/templates/skills/ark-adopt.md +91 -6
- package/templates/skills/ark-architect.md +5 -18
- package/templates/skills/ark-autopilot.md +14 -6
- package/templates/skills/ark-contract.md +9 -20
- package/templates/skills/ark-coverage.md +13 -9
- package/templates/skills/ark-explain.md +11 -5
- package/templates/skills/ark-explore.md +39 -5
- package/templates/skills/ark-fix.md +15 -20
- package/templates/skills/ark-loop.md +14 -20
- package/templates/skills/ark-order.md +200 -0
- package/templates/skills/ark-place.md +49 -11
- package/templates/skills/ark-runtime.md +21 -6
- package/templates/skills/ark-think.md +24 -126
- package/templates/skills/ark-upgrade.md +14 -3
- package/templates/tests/ark-adoption-gaps.test.ts +5 -4
|
@@ -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',
|
|
@@ -37,6 +37,8 @@ import {
|
|
|
37
37
|
hasCheckArchitectureScript,
|
|
38
38
|
ensureTypecheckScript,
|
|
39
39
|
compactRouterHost,
|
|
40
|
+
mergeArkMcpJson,
|
|
41
|
+
MCP_JSON_GATE_FILES,
|
|
40
42
|
writeTemplate,
|
|
41
43
|
} from './gate-files.mjs';
|
|
42
44
|
import {
|
|
@@ -573,6 +575,27 @@ export function runInstallAgentGates(args) {
|
|
|
573
575
|
// Upsert ark-write-gate without requiring --force; never wipe sibling named hooks.
|
|
574
576
|
return writeTemplate(root, relativePath, merged, true);
|
|
575
577
|
}
|
|
578
|
+
if (MCP_JSON_GATE_FILES.includes(relativePath)) {
|
|
579
|
+
const fullPath = path.join(root, relativePath);
|
|
580
|
+
let existing = '';
|
|
581
|
+
try {
|
|
582
|
+
existing = fs.readFileSync(fullPath, 'utf8');
|
|
583
|
+
} catch {
|
|
584
|
+
// Missing MCP JSON → write generated ark server.
|
|
585
|
+
}
|
|
586
|
+
if (!existing) {
|
|
587
|
+
return writeTemplate(root, relativePath, content, true);
|
|
588
|
+
}
|
|
589
|
+
const merged = mergeArkMcpJson(existing, content);
|
|
590
|
+
if (merged == null) {
|
|
591
|
+
return args.force
|
|
592
|
+
? writeTemplate(root, relativePath, content, true)
|
|
593
|
+
: { relativePath, status: 'skipped-non-ark' };
|
|
594
|
+
}
|
|
595
|
+
if (merged === existing) return { relativePath, status: 'skipped' };
|
|
596
|
+
// Upsert mcpServers.ark without requiring --force; never wipe sibling servers.
|
|
597
|
+
return writeTemplate(root, relativePath, merged, true);
|
|
598
|
+
}
|
|
576
599
|
return writeTemplate(
|
|
577
600
|
root,
|
|
578
601
|
relativePath,
|
|
@@ -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
|
}
|
|
@@ -308,13 +308,69 @@ export function emitHostAllow(output, { antigravityStyle, cursorStyle }) {
|
|
|
308
308
|
emitCursorAllow(output, cursorStyle);
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
/** Host-native deny envelopes. Exit 2 is still set by the caller. */
|
|
312
|
+
export function emitHostDeny(output, { antigravityStyle, cursorStyle, grokStyle, message, file }) {
|
|
313
|
+
const text = String(message || '').endsWith('\n') ? String(message) : `${message}\n`;
|
|
314
|
+
output.stderr(text);
|
|
315
|
+
if (antigravityStyle || grokStyle) {
|
|
316
|
+
output.stdout(`${JSON.stringify({ decision: 'deny', reason: String(message || '').trim() })}\n`);
|
|
317
|
+
}
|
|
318
|
+
if (cursorStyle) {
|
|
319
|
+
output.stdout(
|
|
320
|
+
JSON.stringify({
|
|
321
|
+
permission: 'deny',
|
|
322
|
+
agent_message: String(message || '').trim(),
|
|
323
|
+
user_message: `ArkGate blocked write to ${file || 'this file'}`,
|
|
324
|
+
}) + '\n'
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** File-local twin of CONFIG_UNCLASSIFIED_FILES — included, no layer, write must not land. */
|
|
330
|
+
export function unclassifiedIncludedWriteDeny(relativePath) {
|
|
331
|
+
const file = String(relativePath || 'this file');
|
|
332
|
+
return {
|
|
333
|
+
ruleId: 'CONFIG_UNCLASSIFIED_FILES',
|
|
334
|
+
message: `${file} is included but matches no layer, so import rules will not run on it.`,
|
|
335
|
+
nextAction:
|
|
336
|
+
'Put it in a layer folder with /ark-place, or extend layer patterns / narrow include.',
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** File-local twin of CONFIG_LAYER_MISSING_OWNER — required owners, this house has none. */
|
|
341
|
+
export function unownedLayerWriteDeny(relativePath, layerName) {
|
|
342
|
+
const file = String(relativePath || 'this file');
|
|
343
|
+
const house = String(layerName || 'this layer');
|
|
344
|
+
return {
|
|
345
|
+
ruleId: 'CONFIG_LAYER_MISSING_OWNER',
|
|
346
|
+
message: `${file} is in ${house}, and that folder has no owner.`,
|
|
347
|
+
nextAction: `Add a GitHub handle or email to ${house}'s owners in ark.config.json (/ark-adopt).`,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Fail-closed write when requireLayerOwners is on and this house has no owners.
|
|
353
|
+
* Silent when the flag is off, the layer is reserved, or owners are present.
|
|
354
|
+
*/
|
|
355
|
+
export function requiredOwnerWriteDeny(config, layerName, relativePath) {
|
|
356
|
+
if (config?.requireLayerOwners !== true || !layerName) return null;
|
|
357
|
+
const layer = (config.layers ?? []).find((entry) => entry?.name === layerName);
|
|
358
|
+
if (!layer) return null;
|
|
359
|
+
if (layer.optional === true || layer.reserved === true || layer.allowEmpty === true) return null;
|
|
360
|
+
const owners = Array.isArray(layer.owners)
|
|
361
|
+
? layer.owners.filter((entry) => typeof entry === 'string' && entry.length > 0)
|
|
362
|
+
: [];
|
|
363
|
+
if (owners.length > 0) return null;
|
|
364
|
+
return unownedLayerWriteDeny(relativePath, layerName);
|
|
365
|
+
}
|
|
366
|
+
|
|
311
367
|
/**
|
|
312
368
|
* Socket-style write-gate deny: two lines first. Pass/fail, no score.
|
|
313
369
|
* Rule id stays on a following line, not the first sentence.
|
|
314
370
|
*/
|
|
315
371
|
export function formatWriteGateDeny({ file, reason, ruleId, nextAction, extraLines = [] }) {
|
|
316
372
|
const target = file || 'this write';
|
|
317
|
-
const why = String(reason || '
|
|
373
|
+
const why = String(reason || 'a bad import — the write doesn’t land').replace(/\s+/g, ' ').trim();
|
|
318
374
|
const next =
|
|
319
375
|
nextAction && /place|move|import|port/i.test(nextAction)
|
|
320
376
|
? nextAction
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Soft no-Domain / all-logic-in-frontend residual (P2 §3 / Guiar).
|
|
3
|
+
* Tooling I/O. Never a gate fail.
|
|
4
|
+
*
|
|
5
|
+
* Projects facts doctor already has: empty Domain-role layer + presentation
|
|
6
|
+
* file share, or the existing `domain-logic-in-ui` smell. No second tree walk.
|
|
7
|
+
* Silent when there is no frontend, Domain already has files, or the bag is
|
|
8
|
+
* too thin to call “all logic in the UI.”
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { isDomainRoleLayerName } from './arkrules-sensors.mjs';
|
|
12
|
+
|
|
13
|
+
export const NO_DOMAIN_FRONTEND_ASK =
|
|
14
|
+
'Business rules live in the UI, and Domain is still empty.';
|
|
15
|
+
|
|
16
|
+
export const NO_DOMAIN_FRONTEND_NEXT =
|
|
17
|
+
'Put one pure rule in a Domain file (/ark-place), then one small refactor with /ark-autopilot. Do not pile more rules in pages.';
|
|
18
|
+
|
|
19
|
+
/** Presentation-role house by name. Align with design-smells presentation heuristic. */
|
|
20
|
+
export function isPresentationRoleLayerName(name) {
|
|
21
|
+
return typeof name === 'string' && /presentation|ui|view|frontend/i.test(name);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function layerRows(input) {
|
|
25
|
+
return Array.isArray(input.coverage?.layers) ? input.coverage.layers : [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function configLayers(input) {
|
|
29
|
+
return Array.isArray(input.config?.layers) ? input.config.layers : [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function emptyLayerSet(input) {
|
|
33
|
+
return new Set(Array.isArray(input.coverage?.emptyLayers) ? input.coverage.emptyLayers : []);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function totalGovernedFiles(input) {
|
|
37
|
+
const governed = input.coverage?.governed?.totalFiles;
|
|
38
|
+
if (Number.isFinite(governed) && governed >= 0) return governed;
|
|
39
|
+
const total = input.coverage?.totalFiles;
|
|
40
|
+
return Number.isFinite(total) && total >= 0 ? total : 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hasDomainLogicInUiSmell(designSmells) {
|
|
44
|
+
return (Array.isArray(designSmells) ? designSmells : []).some(
|
|
45
|
+
(smell) => smell && smell.id === 'domain-logic-in-ui'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Soft residual when Domain is declared but empty and the UI holds the rules.
|
|
51
|
+
* Never flips valid / goal.met.
|
|
52
|
+
*
|
|
53
|
+
* @param {{ config?: object, coverage?: object, designSmells?: object[] }} [input]
|
|
54
|
+
* @returns {{ kind: 'ui-logic' | 'presentation-bag', ask: string, nextAction: string, domainLayers: string[], presentationFiles: number } | null}
|
|
55
|
+
*/
|
|
56
|
+
export function collectNoDomainFrontendResidual(input = {}) {
|
|
57
|
+
const totalFiles = totalGovernedFiles(input);
|
|
58
|
+
if (totalFiles <= 0) return null;
|
|
59
|
+
|
|
60
|
+
const declared = configLayers(input);
|
|
61
|
+
const domainLayers = declared.filter((layer) =>
|
|
62
|
+
isDomainRoleLayerName(layer.name, layer.intentPrefixes ?? [])
|
|
63
|
+
);
|
|
64
|
+
if (domainLayers.length === 0) return null;
|
|
65
|
+
|
|
66
|
+
const rows = layerRows(input);
|
|
67
|
+
const empty = emptyLayerSet(input);
|
|
68
|
+
const domainEmpty = domainLayers.every((layer) => {
|
|
69
|
+
const row = rows.find((entry) => entry.name === layer.name);
|
|
70
|
+
const files = Number(row?.files) || 0;
|
|
71
|
+
return files === 0 || empty.has(layer.name);
|
|
72
|
+
});
|
|
73
|
+
if (!domainEmpty) return null;
|
|
74
|
+
|
|
75
|
+
const presentationFiles = rows
|
|
76
|
+
.filter((row) => isPresentationRoleLayerName(row.name))
|
|
77
|
+
.reduce((sum, row) => sum + (Number(row.files) || 0), 0);
|
|
78
|
+
if (presentationFiles <= 0) return null;
|
|
79
|
+
|
|
80
|
+
const uiLogic = hasDomainLogicInUiSmell(input.designSmells);
|
|
81
|
+
const presentationShare = presentationFiles / totalFiles;
|
|
82
|
+
if (!uiLogic && (presentationShare < 0.5 || presentationFiles < 3)) return null;
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
kind: uiLogic ? 'ui-logic' : 'presentation-bag',
|
|
86
|
+
ask: NO_DOMAIN_FRONTEND_ASK,
|
|
87
|
+
nextAction: NO_DOMAIN_FRONTEND_NEXT,
|
|
88
|
+
domainLayers: domainLayers.map((layer) => layer.name),
|
|
89
|
+
presentationFiles,
|
|
90
|
+
};
|
|
91
|
+
}
|
package/bin/lib/presets.mjs
CHANGED
|
@@ -670,8 +670,7 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
670
670
|
},
|
|
671
671
|
{
|
|
672
672
|
name: 'ApplicationOrchestration',
|
|
673
|
-
description:
|
|
674
|
-
'Use cases and services that coordinate the domain through ports. Next App Router API (`app/api/**`) and Pages API (`pages/api/**`) are orchestration shells, not UI.',
|
|
673
|
+
description: 'Use cases and services that coordinate the domain through ports.',
|
|
675
674
|
patterns: [
|
|
676
675
|
'**/application/**',
|
|
677
676
|
'**/use-cases/**',
|
|
@@ -687,7 +686,7 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
687
686
|
{
|
|
688
687
|
name: 'PresentationAdapters',
|
|
689
688
|
description:
|
|
690
|
-
'Entrypoints — UI,
|
|
689
|
+
'Entrypoints — UI, pages, controllers when present. Never bare lib/** (data clients are Persistence).',
|
|
691
690
|
patterns: [
|
|
692
691
|
'**/app/**',
|
|
693
692
|
'**/pages/**',
|
|
@@ -760,7 +759,7 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
760
759
|
{
|
|
761
760
|
name: 'ApplicationOrchestration',
|
|
762
761
|
description:
|
|
763
|
-
'Server actions, features,
|
|
762
|
+
'Server actions, features, and non-UI lib orchestration (when present).',
|
|
764
763
|
patterns: [
|
|
765
764
|
'src/features/**',
|
|
766
765
|
'src/server/**',
|
package/bin/lib/product-copy.mjs
CHANGED
|
@@ -6,6 +6,25 @@
|
|
|
6
6
|
* common software words. See docs/product-voice.md.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
/** First-contact north star. Ordinary English. Spanish names once. */
|
|
10
|
+
export const NORTH_STAR_ONE_LINE =
|
|
11
|
+
'Contener · Guiar · Ordenar — contain the write, guide the next step, order leftover mess.';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Compact doctor / details — only when `arkRules` is on.
|
|
15
|
+
* Absence stays silent (unlike ArkOrder, which speaks when off).
|
|
16
|
+
*/
|
|
17
|
+
export const ARKRULES_ONE_BREATH =
|
|
18
|
+
'Layers stop a bad import. ArkRules is optional policies inside one folder.';
|
|
19
|
+
|
|
20
|
+
/** Next step when the map is on and something still needs a human. */
|
|
21
|
+
export const ARKRULES_FIRST_CONTACT_NEXT =
|
|
22
|
+
'Next: ark-check --rules-inventory — then /ark-adopt to write one rule, or /ark-autopilot to promote.';
|
|
23
|
+
|
|
24
|
+
/** Next step when Domain has code but invariants[] is empty. */
|
|
25
|
+
export const ARKRULES_EMPTY_CATALOG_NEXT =
|
|
26
|
+
'Next: add 1–2 short phrases to invariants[] in the Domain rules file, then ark-check --doctor.';
|
|
27
|
+
|
|
9
28
|
/** Status-light leftover-design qualifier (was “design-weak”). */
|
|
10
29
|
export const LEFTOVER_DESIGN_LABEL = 'leftover design work';
|
|
11
30
|
|