arkgate 4.8.8 → 4.8.10
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 +141 -3
- package/README.md +9 -6
- package/bin/ark-check-runtime.mjs +24 -2
- package/bin/ark-layer-match.mjs +25 -10
- package/bin/ark.mjs +18 -10
- package/bin/lib/agent-homes.mjs +1 -1
- package/bin/lib/analysis-engine.mjs +8 -8
- package/bin/lib/architecture-scan.mjs +91 -4
- package/bin/lib/ark-order-facts.mjs +11 -4
- package/bin/lib/ark-order-sensors.mjs +103 -3
- package/bin/lib/arkrule-file-hints.mjs +255 -20
- package/bin/lib/arkrules-sensors.mjs +364 -68
- package/bin/lib/baseline-key.mjs +45 -1
- package/bin/lib/config-contract.mjs +9 -3
- package/bin/lib/config-extras.mjs +1 -0
- package/bin/lib/contract-smells.mjs +12 -6
- package/bin/lib/diagnostic-catalog.mjs +1 -0
- package/bin/lib/doctor-human.mjs +35 -10
- package/bin/lib/doctor-next-actions.mjs +24 -3
- package/bin/lib/field-install.mjs +23 -2
- package/bin/lib/first-run-help.mjs +69 -5
- package/bin/lib/gate-files.mjs +108 -22
- package/bin/lib/managed-upgrade.mjs +9 -1
- package/bin/lib/resolved-candidate-facts.mjs +82 -1
- package/bin/lib/rules-inventory.mjs +7 -3
- package/bin/lib/upgrade-command.mjs +17 -4
- package/bin/lib/upstream-report.mjs +330 -0
- package/bin/lib/violations.mjs +51 -15
- package/dist/{configTypes-0eHpocR3.d.ts → configTypes-j7so8B4O.d.ts} +12 -0
- package/dist/{diagnosticCatalog-DxKCTBbp.d.ts → diagnosticCatalog-biferT4R.d.ts} +11 -5
- package/dist/eslint/index.cjs +5 -8
- package/dist/eslint/index.d.ts +6 -4
- package/dist/eslint/index.js +5 -8
- package/dist/index.cjs +30 -33
- package/dist/index.d.ts +18 -6
- package/dist/index.js +30 -33
- package/dist/nestjs/index.cjs +3 -3
- package/dist/nestjs/index.d.ts +3 -3
- package/dist/nestjs/index.js +2 -2
- package/dist/order/index.cjs +1 -1
- package/dist/order/index.d.ts +6 -2
- package/dist/order/index.js +1 -1
- package/dist/runtime/index.cjs +11 -11
- package/dist/runtime/index.d.ts +6 -6
- package/dist/runtime/index.js +11 -11
- package/dist/{types-BK47clMl.d.ts → types-Djbs3KjE.d.ts} +1 -1
- package/dist/{types-DxvmJO-D.d.ts → types-tGhZUiGX.d.ts} +1 -1
- package/docs/README.md +4 -3
- package/docs/agent-guide.md +27 -2
- package/docs/ai-gates.md +8 -0
- package/docs/arkorder.md +30 -7
- package/docs/brownfield-adoption.md +30 -0
- package/docs/configuration.md +61 -14
- package/docs/develop.md +4 -2
- package/docs/diagnostics.md +10 -0
- package/docs/package-surface.md +7 -5
- package/docs/use.md +11 -0
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +12 -2
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-contract/SKILL.md +1 -1
- package/templates/agent-skills/ark-explore/SKILL.md +24 -3
- package/templates/skills/ark-contract.md +1 -1
- package/templates/skills/ark-explore.md +24 -3
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Pure CLI helper (bin/lib/rules-inventory.mjs). Zero Node I/O.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { DOMAIN_EVENTS_PUSH_RE, DOMAIN_INVARIANT_WORD_RE, expectedDomainInvariantWordsPhrase, isIdiomaticEventsReset, } from './arkrules-sensors.mjs';
|
|
11
12
|
function lineOf(content, index) {
|
|
12
13
|
return content.slice(0, index).split('\n').length;
|
|
13
14
|
}
|
|
@@ -226,7 +227,7 @@ export function buildRulesInventory(input) {
|
|
|
226
227
|
/(?:^|\/)[^/]*(?:-access)?\.error\./i.test(posix) ||
|
|
227
228
|
/(?:^|\/)errors?(?:\/|$)/i.test(posix);
|
|
228
229
|
if (!isErrorBag) {
|
|
229
|
-
const mutRe = /
|
|
230
|
+
const mutRe = /\bthis\.[A-Za-z_][A-Za-z0-9_]*\s*=(?!=)/g;
|
|
230
231
|
let mut;
|
|
231
232
|
while ((mut = mutRe.exec(content)) !== null) {
|
|
232
233
|
const classStart = content.lastIndexOf('class ', mut.index);
|
|
@@ -239,15 +240,18 @@ export function buildRulesInventory(input) {
|
|
|
239
240
|
if (/\bextends\s+(?:Error|[A-Za-z_$][A-Za-z0-9_$]*Error)\b/.test(classHeader)) {
|
|
240
241
|
continue;
|
|
241
242
|
}
|
|
243
|
+
if (isIdiomaticEventsReset(content, mut.index))
|
|
244
|
+
continue;
|
|
242
245
|
const window = content.slice(Math.max(0, mut.index - 200), mut.index + 200);
|
|
243
|
-
if (
|
|
246
|
+
if (!DOMAIN_INVARIANT_WORD_RE.test(window) &&
|
|
247
|
+
!DOMAIN_EVENTS_PUSH_RE.test(window)) {
|
|
244
248
|
seq += 1;
|
|
245
249
|
candidates.push({
|
|
246
250
|
id: `inv-mut-${seq}`,
|
|
247
251
|
kind: 'mutation-without-guard',
|
|
248
252
|
file,
|
|
249
253
|
line: lineOf(content, mut.index),
|
|
250
|
-
message:
|
|
254
|
+
message: `Domain field mutation without nearby ${expectedDomainInvariantWordsPhrase()}.`,
|
|
251
255
|
confidence: 'heuristic',
|
|
252
256
|
governedLayer,
|
|
253
257
|
suggestedArkRule: {
|
|
@@ -603,10 +603,23 @@ export function runUpgradeCommand(args, dependencies) {
|
|
|
603
603
|
return 2;
|
|
604
604
|
}
|
|
605
605
|
if (applied.blocked) {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
606
|
+
const command = buildUpgradeNextCommand(
|
|
607
|
+
{ ...args, acceptConflicts: true },
|
|
608
|
+
applied.planDigest
|
|
609
|
+
);
|
|
610
|
+
if (args.json) {
|
|
611
|
+
console.log(
|
|
612
|
+
managedUpgradeJson(applied, {
|
|
613
|
+
blocked: true,
|
|
614
|
+
reasonCode: applied.reasonCode ?? 'managed-consent-required',
|
|
615
|
+
nextCommand: command,
|
|
616
|
+
})
|
|
617
|
+
);
|
|
618
|
+
} else {
|
|
619
|
+
renderManagedUpgrade(applied, {
|
|
620
|
+
next: 'Preview again with --accept-conflicts, then use that preview\'s exact next command.',
|
|
621
|
+
});
|
|
622
|
+
}
|
|
610
623
|
return 1;
|
|
611
624
|
}
|
|
612
625
|
if (applied.nothingToApply && !applied.applied) {
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upstream GitHub issue draft for ArkGate itself (`arkgate report` / `ark report`).
|
|
3
|
+
* Target is this package's package.json bugs.url (pedroknigge/arkgate), never the consumer repo.
|
|
4
|
+
*/
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import readline from 'node:readline/promises';
|
|
9
|
+
|
|
10
|
+
import { __packageRoot } from './gate-files.mjs';
|
|
11
|
+
|
|
12
|
+
export const UPSTREAM_OWNER_REPO = 'pedroknigge/arkgate';
|
|
13
|
+
export const SUBMIT_PROMPT = 'Type submit to send';
|
|
14
|
+
|
|
15
|
+
export function ownerRepoFromGithubUrl(url) {
|
|
16
|
+
if (typeof url !== 'string' || url.trim() === '') return null;
|
|
17
|
+
const match = url.trim().match(/github\.com[:/]+([^/]+)\/([^/#?\s]+)/i);
|
|
18
|
+
if (!match) return null;
|
|
19
|
+
const owner = match[1];
|
|
20
|
+
const repo = match[2].replace(/\.git$/i, '');
|
|
21
|
+
if (!owner || !repo) return null;
|
|
22
|
+
return `${owner}/${repo}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveUpstreamRepo(pkg) {
|
|
26
|
+
const fromBugs = ownerRepoFromGithubUrl(pkg?.bugs?.url);
|
|
27
|
+
if (fromBugs) return fromBugs;
|
|
28
|
+
const repository = pkg?.repository;
|
|
29
|
+
const repoUrl = typeof repository === 'string' ? repository : repository?.url;
|
|
30
|
+
const fromRepo = ownerRepoFromGithubUrl(repoUrl);
|
|
31
|
+
if (fromRepo) return fromRepo;
|
|
32
|
+
return UPSTREAM_OWNER_REPO;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function posixSingleQuote(value) {
|
|
36
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatGhIssueCreateCommand({ repo, title, body }) {
|
|
40
|
+
return `gh issue create --repo ${posixSingleQuote(repo)} --title ${posixSingleQuote(title)} --body ${posixSingleQuote(body)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function reportUsage() {
|
|
44
|
+
return `arkgate report (alias ark report) — draft an upstream GitHub issue for ArkGate.
|
|
45
|
+
|
|
46
|
+
Target: pedroknigge/arkgate (this package's package.json bugs.url). Never the consumer repo.
|
|
47
|
+
|
|
48
|
+
Usage:
|
|
49
|
+
arkgate report [--root <project>] [--json] [--title <text>] [--finding <ref>]
|
|
50
|
+
arkgate report --submit --i-confirm-submit
|
|
51
|
+
arkgate report --submit # TTY: type submit to send
|
|
52
|
+
|
|
53
|
+
Default prints a draft (arkgate version + last-check snippet). Nothing is created.
|
|
54
|
+
Create only with --submit AND (--i-confirm-submit after the human said yes, or TTY
|
|
55
|
+
"${SUBMIT_PROMPT}"). --yes does not submit.
|
|
56
|
+
|
|
57
|
+
If gh is missing or not logged in: prints the draft and the exact
|
|
58
|
+
gh issue create --repo pedroknigge/arkgate command, then exits 2.
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function lastCheckSnippet(latest) {
|
|
63
|
+
if (!latest || typeof latest !== 'object') return null;
|
|
64
|
+
const at =
|
|
65
|
+
typeof latest.generatedAt === 'string'
|
|
66
|
+
? latest.generatedAt
|
|
67
|
+
: typeof latest.at === 'string'
|
|
68
|
+
? latest.at
|
|
69
|
+
: null;
|
|
70
|
+
const active =
|
|
71
|
+
typeof latest.activeViolations === 'number'
|
|
72
|
+
? latest.activeViolations
|
|
73
|
+
: typeof latest.violations?.active === 'number'
|
|
74
|
+
? latest.violations.active
|
|
75
|
+
: null;
|
|
76
|
+
let verdict = null;
|
|
77
|
+
if (latest.ok === true && (active == null || active === 0)) verdict = 'pass';
|
|
78
|
+
else if (latest.ok === false || (typeof active === 'number' && active > 0)) verdict = 'fail';
|
|
79
|
+
else if (latest.completeness === 'partial' || latest.completeness === 'unavailable') {
|
|
80
|
+
verdict = 'incomplete';
|
|
81
|
+
} else if (latest.ok === true) verdict = 'pass';
|
|
82
|
+
if (at == null && verdict == null && active == null) return null;
|
|
83
|
+
return { at, verdict, activeViolations: active };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function readLastCheckSnapshot(root) {
|
|
87
|
+
const latestPath = path.join(root, '.ark', 'reports', 'latest.json');
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(fs.readFileSync(latestPath, 'utf8'));
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function buildIssueDraft({ repo, arkgateVersion, lastCheck, finding, title }) {
|
|
96
|
+
const resolvedTitle =
|
|
97
|
+
typeof title === 'string' && title.trim()
|
|
98
|
+
? title.trim()
|
|
99
|
+
: finding
|
|
100
|
+
? `ArkGate finding ${finding}`
|
|
101
|
+
: 'ArkGate field report';
|
|
102
|
+
const lines = [`## ArkGate version`, String(arkgateVersion ?? 'unknown'), ''];
|
|
103
|
+
if (finding) {
|
|
104
|
+
lines.push('## Finding', String(finding), '');
|
|
105
|
+
}
|
|
106
|
+
if (lastCheck) {
|
|
107
|
+
lines.push('## Last check');
|
|
108
|
+
lines.push(`- at: ${lastCheck.at ?? 'none'}`);
|
|
109
|
+
lines.push(`- verdict: ${lastCheck.verdict ?? 'none'}`);
|
|
110
|
+
lines.push(`- activeViolations: ${lastCheck.activeViolations ?? 'unknown'}`);
|
|
111
|
+
lines.push('');
|
|
112
|
+
} else {
|
|
113
|
+
lines.push('## Last check', 'No last-check snapshot under .ark/reports/latest.json.', '');
|
|
114
|
+
}
|
|
115
|
+
lines.push(
|
|
116
|
+
'## Repro',
|
|
117
|
+
'```bash',
|
|
118
|
+
'npx arkgate-check --doctor',
|
|
119
|
+
'npx arkgate status --json',
|
|
120
|
+
'```',
|
|
121
|
+
'',
|
|
122
|
+
'## What happened',
|
|
123
|
+
'(ArkGate bug, false green, false red, missing doc, or improvable behavior in ArkGate itself — not leftover design in the consumer app.)',
|
|
124
|
+
'',
|
|
125
|
+
`Prepared with \`arkgate report\` against upstream ${repo}. Not the consumer repo.`
|
|
126
|
+
);
|
|
127
|
+
return { repo, title: resolvedTitle, body: lines.join('\n') };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function defaultRunGh(argv, options = {}) {
|
|
131
|
+
const env = { ...(options.env ?? process.env) };
|
|
132
|
+
delete env.GH_REPO;
|
|
133
|
+
const result = spawnSync('gh', argv, {
|
|
134
|
+
encoding: 'utf8',
|
|
135
|
+
env,
|
|
136
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
137
|
+
});
|
|
138
|
+
if (result.error && (result.error.code === 'ENOENT' || result.error.code === 'EACCES')) {
|
|
139
|
+
return { missing: true, status: 127, stdout: '', stderr: result.error.message };
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
missing: false,
|
|
143
|
+
status: result.status ?? 1,
|
|
144
|
+
stdout: result.stdout ?? '',
|
|
145
|
+
stderr: result.stderr ?? '',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function loadArkgatePackageJson(explicit) {
|
|
150
|
+
if (explicit && typeof explicit === 'object') return explicit;
|
|
151
|
+
try {
|
|
152
|
+
return JSON.parse(fs.readFileSync(path.join(__packageRoot, 'package.json'), 'utf8'));
|
|
153
|
+
} catch {
|
|
154
|
+
return { bugs: { url: `https://github.com/${UPSTREAM_OWNER_REPO}/issues` } };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function packageVersion(pkg, fallback) {
|
|
159
|
+
if (typeof fallback === 'string' && fallback.trim()) return fallback;
|
|
160
|
+
return typeof pkg?.version === 'string' ? pkg.version : 'unknown';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function renderHumanDraft(draft, arkgateVersion) {
|
|
164
|
+
return [
|
|
165
|
+
`Upstream: ${draft.repo}`,
|
|
166
|
+
'(never the consumer repo)',
|
|
167
|
+
'',
|
|
168
|
+
`ArkGate version: ${arkgateVersion}`,
|
|
169
|
+
'',
|
|
170
|
+
`Title: ${draft.title}`,
|
|
171
|
+
'',
|
|
172
|
+
draft.body,
|
|
173
|
+
].join('\n');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function defaultPromptSubmit(stdin, stdout) {
|
|
177
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
178
|
+
try {
|
|
179
|
+
return await rl.question(`${SUBMIT_PROMPT}\n`);
|
|
180
|
+
} finally {
|
|
181
|
+
rl.close();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function parseReportArgv(argv = []) {
|
|
186
|
+
const out = {
|
|
187
|
+
submit: false,
|
|
188
|
+
iConfirmSubmit: false,
|
|
189
|
+
json: false,
|
|
190
|
+
yes: false,
|
|
191
|
+
help: false,
|
|
192
|
+
finding: undefined,
|
|
193
|
+
title: undefined,
|
|
194
|
+
root: undefined,
|
|
195
|
+
};
|
|
196
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
197
|
+
const arg = argv[i];
|
|
198
|
+
const next = () => {
|
|
199
|
+
const value = argv[i + 1];
|
|
200
|
+
if (value === undefined || String(value).startsWith('-')) {
|
|
201
|
+
throw new Error(`Missing value for ${arg}. Run arkgate report --help for usage.`);
|
|
202
|
+
}
|
|
203
|
+
i += 1;
|
|
204
|
+
return value;
|
|
205
|
+
};
|
|
206
|
+
if (arg === '--submit') out.submit = true;
|
|
207
|
+
else if (arg === '--i-confirm-submit') out.iConfirmSubmit = true;
|
|
208
|
+
else if (arg === '--json') out.json = true;
|
|
209
|
+
else if (arg === '--yes' || arg === '-y') out.yes = true;
|
|
210
|
+
else if (arg === '--help' || arg === '-h' || arg === 'help') out.help = true;
|
|
211
|
+
else if (arg === '--finding') out.finding = next();
|
|
212
|
+
else if (arg === '--title') out.title = next();
|
|
213
|
+
else if (arg === '--root') out.root = path.resolve(next());
|
|
214
|
+
else throw new Error(`Unknown argument: ${arg}. Run arkgate report --help for usage.`);
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* @param {object} options
|
|
221
|
+
* @returns {Promise<number>} process exit code
|
|
222
|
+
*/
|
|
223
|
+
export async function runUpstreamReportCommand(options = {}) {
|
|
224
|
+
const fromArgv = Array.isArray(options.argv) ? parseReportArgv(options.argv) : {};
|
|
225
|
+
if (fromArgv.help) {
|
|
226
|
+
const writeOut = options.writeOut ?? ((text) => console.log(text));
|
|
227
|
+
writeOut(reportUsage());
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
const root = path.resolve(fromArgv.root ?? options.root ?? process.cwd());
|
|
231
|
+
const json = Boolean(fromArgv.json || options.json);
|
|
232
|
+
const submit = Boolean(fromArgv.submit || options.submit);
|
|
233
|
+
const iConfirmSubmit = Boolean(fromArgv.iConfirmSubmit || options.iConfirmSubmit);
|
|
234
|
+
const yes = Boolean(fromArgv.yes || options.yes);
|
|
235
|
+
const finding = fromArgv.finding ?? options.finding;
|
|
236
|
+
const title = fromArgv.title ?? options.title;
|
|
237
|
+
const stdin = options.stdin ?? process.stdin;
|
|
238
|
+
const stdout = options.stdout ?? process.stdout;
|
|
239
|
+
const stdinIsTTY = options.stdinIsTTY ?? Boolean(stdin.isTTY);
|
|
240
|
+
const env = options.env ?? process.env;
|
|
241
|
+
const runGh = options.runGh ?? ((argv) => defaultRunGh(argv, { env }));
|
|
242
|
+
const writeOut = options.writeOut ?? ((text) => console.log(text));
|
|
243
|
+
const writeErr = options.writeErr ?? ((text) => console.error(text));
|
|
244
|
+
|
|
245
|
+
const pkg = loadArkgatePackageJson(options.arkgatePackageJson);
|
|
246
|
+
const repo = resolveUpstreamRepo(pkg);
|
|
247
|
+
const arkgateVersion = packageVersion(pkg, options.arkgateVersion);
|
|
248
|
+
const lastCheck = lastCheckSnippet(options.latestSnapshot ?? readLastCheckSnapshot(root));
|
|
249
|
+
const draft = buildIssueDraft({
|
|
250
|
+
repo,
|
|
251
|
+
arkgateVersion,
|
|
252
|
+
lastCheck,
|
|
253
|
+
finding,
|
|
254
|
+
title,
|
|
255
|
+
});
|
|
256
|
+
const ghCommand = formatGhIssueCreateCommand(draft);
|
|
257
|
+
const human = renderHumanDraft(draft, arkgateVersion);
|
|
258
|
+
|
|
259
|
+
const payload = {
|
|
260
|
+
schemaVersion: '1.0',
|
|
261
|
+
command: 'report',
|
|
262
|
+
created: false,
|
|
263
|
+
submitted: false,
|
|
264
|
+
repo: draft.repo,
|
|
265
|
+
title: draft.title,
|
|
266
|
+
body: draft.body,
|
|
267
|
+
arkgateVersion,
|
|
268
|
+
lastCheck,
|
|
269
|
+
ghCommand,
|
|
270
|
+
yesDoesNotSubmit: true,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const emit = (extraHuman, extraPayload) => {
|
|
274
|
+
if (json) writeOut(JSON.stringify({ ...payload, ...extraPayload }, null, 2));
|
|
275
|
+
else writeOut(extraHuman ? `${human}\n\n${extraHuman}` : human);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
if (!submit) {
|
|
279
|
+
emit('Draft only. Nothing was created.\nAfter a human confirms: arkgate report --submit --i-confirm-submit\n--yes does not submit.');
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// --yes never confirms. Confirm is --i-confirm-submit or TTY "submit".
|
|
284
|
+
let confirmed = iConfirmSubmit;
|
|
285
|
+
if (!confirmed && stdinIsTTY) {
|
|
286
|
+
const typed = options.promptSubmit
|
|
287
|
+
? await options.promptSubmit()
|
|
288
|
+
: await defaultPromptSubmit(stdin, stdout);
|
|
289
|
+
confirmed = /^\s*submit\s*$/i.test(String(typed ?? ''));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!confirmed) {
|
|
293
|
+
const refused =
|
|
294
|
+
'Refused: --submit requires --i-confirm-submit or typing submit on a TTY. --yes does not submit.';
|
|
295
|
+
if (json) {
|
|
296
|
+
writeOut(JSON.stringify({ ...payload, error: 'submit-confirm-required', yesDoesNotSubmit: true }, null, 2));
|
|
297
|
+
} else {
|
|
298
|
+
writeOut(`${human}\n\nDraft only. ${refused}`);
|
|
299
|
+
}
|
|
300
|
+
writeErr(refused);
|
|
301
|
+
return 2;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const auth = runGh(['auth', 'status']);
|
|
305
|
+
const ghUnavailable = Boolean(auth?.missing) || auth?.status !== 0;
|
|
306
|
+
if (ghUnavailable) {
|
|
307
|
+
const missing =
|
|
308
|
+
'Not filed: gh is missing or not logged in.\nExact command:\n' + ghCommand;
|
|
309
|
+
emit(missing, { created: false, submitted: false, error: 'gh-unavailable', ghCommand });
|
|
310
|
+
if (!json) writeErr('Not filed: gh is missing or not logged in.');
|
|
311
|
+
return 2;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const created = runGh(['issue', 'create', '--repo', repo, '--title', draft.title, '--body', draft.body]);
|
|
315
|
+
if (created?.missing || created?.status !== 0) {
|
|
316
|
+
const missing =
|
|
317
|
+
'Not filed: gh is missing or not logged in.\nExact command:\n' + ghCommand;
|
|
318
|
+
emit(missing, { created: false, submitted: false, error: 'gh-unavailable', ghCommand });
|
|
319
|
+
if (!json) writeErr('Not filed: gh is missing or not logged in.');
|
|
320
|
+
return 2;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const url = String(created.stdout ?? '').trim();
|
|
324
|
+
emit(url ? `Created: ${url}` : 'Created.', {
|
|
325
|
+
created: true,
|
|
326
|
+
submitted: true,
|
|
327
|
+
url: url || null,
|
|
328
|
+
});
|
|
329
|
+
return 0;
|
|
330
|
+
}
|
package/bin/lib/violations.mjs
CHANGED
|
@@ -16,8 +16,12 @@ import { baselineKeysFromDocument, baselineRecordsDocument } from './team-parlia
|
|
|
16
16
|
import { toAdapterDiagnostic } from './adapter-contract.mjs';
|
|
17
17
|
export { baselineKey, baselineOccurrenceKeys };
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
/** Real regenerate flags: freeze-refuse needs --force; law PR needs --contract-session; stewards[] needs --author. */
|
|
20
|
+
export const REGENERATE_BASELINE_COMMAND =
|
|
21
|
+
'ark-check --update-baseline --force --contract-session --author <steward>';
|
|
22
|
+
|
|
23
|
+
export const BASELINE_NOTE =
|
|
24
|
+
`Frozen ark-check violations (one record per edge). Only NEW keys vs the merge-base fail --against / --baseline. Regenerate with: ${REGENERATE_BASELINE_COMMAND} (--author when stewards[] is set)`;
|
|
21
25
|
|
|
22
26
|
export function readBaseline(root, baselinePath) {
|
|
23
27
|
const fullPath = path.isAbsolute(baselinePath) ? baselinePath : path.join(root, baselinePath);
|
|
@@ -76,13 +80,34 @@ export function printViolation(violation) {
|
|
|
76
80
|
export const CONCENTRATION_MIN_VIOLATIONS = 10;
|
|
77
81
|
export const CONCENTRATION_SHARE = 0.9;
|
|
78
82
|
|
|
83
|
+
/** Sensor id for STRUCTURE findings. Orchestration-only is code debt, not a missing toLayer. */
|
|
84
|
+
export function violationSensorId(violation) {
|
|
85
|
+
if (typeof violation?.sensor === 'string' && violation.sensor.trim()) return violation.sensor.trim();
|
|
86
|
+
if (typeof violation?.arkruleId === 'string' && violation.arkruleId.trim()) return violation.arkruleId.trim();
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
79
90
|
export function violationEdge(violation) {
|
|
80
91
|
if (violation.ruleId === 'CIRCULAR_DEPENDENCY') return 'circular dependency';
|
|
81
92
|
if (violation.ruleId === 'FORBIDDEN_GLOBAL') return `${violation.fromLayer ?? '?'} → ambient global`;
|
|
82
93
|
if (violation.fromLayer && violation.toLayer) return `${violation.fromLayer} → ${violation.toLayer}`;
|
|
94
|
+
const sensor = violationSensorId(violation);
|
|
95
|
+
if (sensor) return sensor;
|
|
83
96
|
return violation.ruleId;
|
|
84
97
|
}
|
|
85
98
|
|
|
99
|
+
/** Layer-import arrows can mean a contract bug. Structural-sensor blobs are code debt. */
|
|
100
|
+
export function isContractConcentrationEdge(edge) {
|
|
101
|
+
return typeof edge === 'string' && edge.includes(' → ');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function isContractStyleViolation(violation) {
|
|
105
|
+
if (violationSensorId(violation)) return false;
|
|
106
|
+
if (violation.ruleId === 'CIRCULAR_DEPENDENCY') return true;
|
|
107
|
+
if (violation.ruleId === 'FORBIDDEN_GLOBAL') return true;
|
|
108
|
+
return Boolean(violation.fromLayer && violation.toLayer);
|
|
109
|
+
}
|
|
110
|
+
|
|
86
111
|
// The directory the offending import lands in — the signal for "where does this edge go?".
|
|
87
112
|
// For a LAYER_IMPORT_VIOLATION the target is a resolved file path; cluster by its dir prefix
|
|
88
113
|
// so `kernel/internal/x` and `kernel/internal/y` collapse to one "into kernel/internal/".
|
|
@@ -100,27 +125,35 @@ export function summarizeViolations(violations) {
|
|
|
100
125
|
for (const violation of violations) {
|
|
101
126
|
if (violation.typeOnly) typeOnly += 1;
|
|
102
127
|
const key = violationEdge(violation);
|
|
103
|
-
const entry = byEdge.get(key) ?? {
|
|
128
|
+
const entry = byEdge.get(key) ?? {
|
|
129
|
+
edge: key,
|
|
130
|
+
count: 0,
|
|
131
|
+
typeOnly: 0,
|
|
132
|
+
targets: new Map(),
|
|
133
|
+
contractStyle: true,
|
|
134
|
+
};
|
|
104
135
|
entry.count += 1;
|
|
105
136
|
if (violation.typeOnly) entry.typeOnly += 1;
|
|
137
|
+
if (!isContractStyleViolation(violation)) entry.contractStyle = false;
|
|
106
138
|
const subtree = violationTargetSubtree(violation);
|
|
107
139
|
if (subtree) entry.targets.set(subtree, (entry.targets.get(subtree) ?? 0) + 1);
|
|
108
140
|
byEdge.set(key, entry);
|
|
109
141
|
}
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
.sort((a, b) => b.count - a.count);
|
|
142
|
+
const ranked = [...byEdge.values()].sort((a, b) => b.count - a.count);
|
|
143
|
+
const edges = ranked.map((entry) => ({
|
|
144
|
+
edge: entry.edge,
|
|
145
|
+
count: entry.count,
|
|
146
|
+
typeOnly: entry.typeOnly,
|
|
147
|
+
topTargets: [...entry.targets.entries()]
|
|
148
|
+
.sort((a, b) => b[1] - a[1])
|
|
149
|
+
.slice(0, 4)
|
|
150
|
+
.map(([dir, count]) => ({ dir, count })),
|
|
151
|
+
}));
|
|
121
152
|
const total = violations.length;
|
|
122
153
|
const dominant = edges[0];
|
|
123
154
|
const dominantShare = total > 0 && dominant ? dominant.count / total : 0;
|
|
155
|
+
const contractConcentration =
|
|
156
|
+
Boolean(ranked[0]?.contractStyle) && isContractConcentrationEdge(ranked[0]?.edge);
|
|
124
157
|
return {
|
|
125
158
|
total,
|
|
126
159
|
// Value edges are real runtime coupling; type-only edges (erased at compile time) are
|
|
@@ -130,7 +163,10 @@ export function summarizeViolations(violations) {
|
|
|
130
163
|
edges,
|
|
131
164
|
dominant: dominant ? dominant.edge : undefined,
|
|
132
165
|
dominantShare,
|
|
133
|
-
concentrated:
|
|
166
|
+
concentrated:
|
|
167
|
+
total >= CONCENTRATION_MIN_VIOLATIONS &&
|
|
168
|
+
dominantShare >= CONCENTRATION_SHARE &&
|
|
169
|
+
contractConcentration,
|
|
134
170
|
};
|
|
135
171
|
}
|
|
136
172
|
|
|
@@ -105,12 +105,24 @@ type ArkConfigArkOrder = {
|
|
|
105
105
|
mode: ArkConfigArkOrderMode;
|
|
106
106
|
planeRoots: string[];
|
|
107
107
|
managedLayers: string[];
|
|
108
|
+
/**
|
|
109
|
+
* Haken cap on one `release()` / `assertXiKeyCap` (default 7).
|
|
110
|
+
* Not a cap on the `xiKeys` watchlist length.
|
|
111
|
+
*/
|
|
108
112
|
maxXiKeys: number;
|
|
109
113
|
/**
|
|
110
114
|
* Slow product keys the team can already name (plan, cost code, protocol).
|
|
111
115
|
* Optional. Empty → `ARKORDER_XI_FIELD_WRITE` stays silent.
|
|
116
|
+
* Repo-wide watchlist — not compared to `maxXiKeys`.
|
|
112
117
|
*/
|
|
113
118
|
xiKeys: string[];
|
|
119
|
+
/**
|
|
120
|
+
* Optional globs that narrow ξ field-write observation inside `managedLayers`.
|
|
121
|
+
* Same glob engine as `layers[].patterns`. Absence or empty → every file in
|
|
122
|
+
* those layers. Non-empty → emit only when the layer is managed AND the file
|
|
123
|
+
* matches at least one glob.
|
|
124
|
+
*/
|
|
125
|
+
appliesTo?: string[];
|
|
114
126
|
};
|
|
115
127
|
type ArkConfig = {
|
|
116
128
|
$schema: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { e as CreateArchitectureProfileOptions, b as ArchitectureProfile, d as ArkCheckConfig, C as CreateArchitectureProfileFromArkConfigOptions, f as CreateElevenLayerArkConfigOptions, i as Policy, j as IntentCreator, I as IntentName } from './types-
|
|
2
|
-
import { A as ArkConfig, c as ArkConfigLoadResult } from './configTypes-
|
|
1
|
+
import { e as CreateArchitectureProfileOptions, b as ArchitectureProfile, d as ArkCheckConfig, C as CreateArchitectureProfileFromArkConfigOptions, f as CreateElevenLayerArkConfigOptions, i as Policy, j as IntentCreator, I as IntentName } from './types-Djbs3KjE.js';
|
|
2
|
+
import { A as ArkConfig, c as ArkConfigLoadResult } from './configTypes-j7so8B4O.js';
|
|
3
3
|
|
|
4
4
|
/** Versioned public result contract shared by every ArkGate enforcement adapter. */
|
|
5
5
|
/**
|
|
@@ -409,7 +409,7 @@ declare const ARK_ANALYSIS_RESULT_SCHEMA: {
|
|
|
409
409
|
};
|
|
410
410
|
|
|
411
411
|
/** ArkGate library version — single source of truth. */
|
|
412
|
-
declare const version = "4.8.
|
|
412
|
+
declare const version = "4.8.10";
|
|
413
413
|
|
|
414
414
|
/**
|
|
415
415
|
* AI Code Gate (basic).
|
|
@@ -1924,12 +1924,18 @@ declare function evaluateArkRuleSensors(input: EvaluateArkRuleSensorsInput): Ark
|
|
|
1924
1924
|
* Rules without appliesTo (whole-layer) never emit this signal.
|
|
1925
1925
|
*/
|
|
1926
1926
|
declare function collectEmptyAppliesToFindings(arkRules: EffectiveArkRules, files: readonly string[]): ArkRuleSensorViolation[];
|
|
1927
|
+
/** Optional resolved-import facts when the collector already classified the specifier. */
|
|
1928
|
+
type ResolvedPersistenceImportFact = {
|
|
1929
|
+
specifier?: string;
|
|
1930
|
+
resolvedFile?: string;
|
|
1931
|
+
layer?: string | null;
|
|
1932
|
+
};
|
|
1927
1933
|
/**
|
|
1928
1934
|
* Pure Tooling/Domain heuristic for orchestration-only / thin-adapter fileHints.
|
|
1929
1935
|
* Prefers false negatives over false positives (ADR 0013 discipline).
|
|
1930
1936
|
* Returns null when neither flag is set (callers may omit the path).
|
|
1931
1937
|
*/
|
|
1932
|
-
declare function deriveArkRuleFileHints(_file: string, content: string): {
|
|
1938
|
+
declare function deriveArkRuleFileHints(_file: string, content: string, resolvedImports?: readonly ResolvedPersistenceImportFact[]): {
|
|
1933
1939
|
orchestrationHeavy?: boolean;
|
|
1934
1940
|
adapterThick?: boolean;
|
|
1935
1941
|
persistenceWrite?: boolean;
|
|
@@ -1937,7 +1943,7 @@ declare function deriveArkRuleFileHints(_file: string, content: string): {
|
|
|
1937
1943
|
/**
|
|
1938
1944
|
* Build fileHints map from path→content. Omits paths with no flags (sparse map).
|
|
1939
1945
|
*/
|
|
1940
|
-
declare function buildArkRuleFileHints(fileContents: Readonly<Record<string, string>>): Record<string, {
|
|
1946
|
+
declare function buildArkRuleFileHints(fileContents: Readonly<Record<string, string>>, resolvedImportsByFile?: Readonly<Record<string, readonly ResolvedPersistenceImportFact[]>>): Record<string, {
|
|
1941
1947
|
orchestrationHeavy?: boolean;
|
|
1942
1948
|
adapterThick?: boolean;
|
|
1943
1949
|
persistenceWrite?: boolean;
|