arkgate 4.8.14 → 4.8.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +133 -0
- package/README.md +13 -5
- package/bin/ark-check-runtime.mjs +17 -49
- package/bin/ark-mcp-runtime.mjs +111 -2
- package/bin/ark-shared.mjs +140 -11
- package/bin/ark.mjs +7 -25
- package/bin/lib/adr-presence.mjs +97 -0
- package/bin/lib/agent-skills-package.mjs +179 -1
- package/bin/lib/analysis-completeness.mjs +38 -2
- package/bin/lib/analysis-engine.mjs +6 -6
- package/bin/lib/architecture-scan.mjs +26 -4
- package/bin/lib/ark-run-doctor.mjs +6 -0
- package/bin/lib/ark-run-report.mjs +6 -2
- package/bin/lib/arkrules-sensors.mjs +81 -0
- package/bin/lib/baseline-key.mjs +4 -1
- package/bin/lib/check-args.mjs +52 -2
- package/bin/lib/config-contract.mjs +64 -1
- package/bin/lib/diagnostic-catalog.mjs +3 -1
- package/bin/lib/doctor-advisories.mjs +94 -10
- package/bin/lib/doctor-human.mjs +18 -5
- package/bin/lib/doctor-next-actions.mjs +14 -0
- package/bin/lib/doctor-plan.mjs +32 -2
- package/bin/lib/enforcement-honesty.mjs +47 -2
- package/bin/lib/first-run-help.mjs +8 -1
- package/bin/lib/host-support-matrix.mjs +9 -1
- package/bin/lib/html-report-depth.mjs +2 -0
- package/bin/lib/html-report.mjs +13 -2
- package/bin/lib/import-resolve.mjs +74 -13
- package/bin/lib/improvement-compass-doctor.mjs +6 -1
- package/bin/lib/improvement-compass-map.mjs +3 -1
- package/bin/lib/layer-description.mjs +90 -8
- package/bin/lib/mcp-hook-payload.mjs +56 -0
- package/bin/lib/no-domain-frontend.mjs +91 -0
- package/bin/lib/presets.mjs +3 -4
- package/bin/lib/product-copy.mjs +15 -0
- package/bin/lib/projected-governed-coverage.mjs +114 -0
- package/bin/lib/recommend-cli.mjs +54 -0
- package/bin/lib/remediation.mjs +4 -0
- package/bin/lib/resolved-candidate-facts.mjs +83 -66
- package/bin/lib/rules-under-contract.mjs +117 -2
- package/bin/lib/snippet-analysis.mjs +14 -8
- package/bin/lib/start-preview.mjs +5 -2
- package/bin/lib/states-transitions-presence.mjs +212 -0
- package/bin/lib/status-command.mjs +2 -0
- package/bin/lib/status-transition-catalog.mjs +410 -0
- package/bin/lib/team-parliament-io.mjs +10 -0
- package/bin/lib/violations.mjs +13 -1
- package/bin/lib/write-path-capabilities.mjs +20 -5
- package/bin/lib/write-path-detect.mjs +27 -2
- package/dist/{configTypes-j7so8B4O.d.ts → configTypes-Dt3DpVbd.d.ts} +19 -0
- package/dist/{diagnosticCatalog-DVx_2RmF.d.ts → diagnosticCatalog-BEg85XlE.d.ts} +3 -3
- package/dist/eslint/index.cjs +4 -4
- package/dist/eslint/index.d.ts +1 -1
- package/dist/eslint/index.js +4 -4
- package/dist/index.cjs +31 -31
- package/dist/index.d.ts +70 -9
- package/dist/index.js +31 -31
- package/dist/nestjs/index.cjs +5 -5
- package/dist/nestjs/index.d.ts +3 -3
- package/dist/nestjs/index.js +5 -5
- package/dist/runtime/index.cjs +15 -15
- package/dist/runtime/index.d.ts +6 -6
- package/dist/runtime/index.js +15 -15
- package/dist/{types-Djbs3KjE.d.ts → types-CN9tVMPz.d.ts} +3 -1
- package/dist/{types-tGhZUiGX.d.ts → types-TBiv0WHL.d.ts} +1 -1
- package/docs/README.md +1 -1
- package/docs/agent-guide.md +6 -4
- package/docs/ai-gates.md +10 -3
- package/docs/brownfield-adoption.md +4 -1
- package/docs/configuration.md +55 -7
- package/docs/develop.md +1 -0
- package/docs/diagnostics.md +23 -3
- package/docs/enthusiast/how-to-agent-gates.md +5 -0
- package/docs/enthusiast/how-to-pick-shape.md +1 -1
- package/docs/package-surface.md +7 -5
- package/docs/use.md +5 -0
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +23 -2
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +82 -1
- package/templates/agent-skills/ark-autopilot/SKILL.md +6 -2
- package/templates/agent-skills/ark-coverage/SKILL.md +1 -1
- package/templates/agent-skills/ark-explain/SKILL.md +4 -2
- package/templates/agent-skills/ark-explore/SKILL.md +14 -1
- package/templates/agent-skills/ark-place/SKILL.md +38 -3
- package/templates/agent-skills/ark-runtime/SKILL.md +4 -2
- package/templates/agent-skills/ark-upgrade/SKILL.md +1 -1
- package/templates/arkrules/DomainModel.json +14 -1
- package/templates/skills/ark-adopt.md +82 -1
- package/templates/skills/ark-autopilot.md +6 -2
- package/templates/skills/ark-coverage.md +1 -1
- package/templates/skills/ark-explain.md +4 -2
- package/templates/skills/ark-explore.md +14 -1
- package/templates/skills/ark-place.md +38 -3
- package/templates/skills/ark-runtime.md +4 -2
- package/templates/skills/ark-upgrade.md +1 -1
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow opt-in status/transition catalog (P2 §2 / Ordenar).
|
|
3
|
+
* Tooling I/O. Never a gate fail.
|
|
4
|
+
*
|
|
5
|
+
* Opt-in: a Domain-role file already has a closed status/state vocabulary
|
|
6
|
+
* (≥2 string literals on a *Status / *State / *Lifecycle type or enum, or a
|
|
7
|
+
* status:/state: field with a literal union). Silent when that shape is
|
|
8
|
+
* absent. Residual only when a domain (or dedicated states) doc is in play
|
|
9
|
+
* AND the map is missing or heading-only — names from the code, not invented.
|
|
10
|
+
*
|
|
11
|
+
* Does not walk docs. Does not scan Application/UI. Does not fail the check.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { layerForFile } from '../ark-shared.mjs';
|
|
17
|
+
import { isDomainRoleLayerName } from './arkrules-sensors.mjs';
|
|
18
|
+
import { collectStatesTransitionsResidual } from './states-transitions-presence.mjs';
|
|
19
|
+
|
|
20
|
+
export const STATUS_CATALOG_ASK =
|
|
21
|
+
'Domain code already names statuses, but the states → transitions map is still thin.';
|
|
22
|
+
|
|
23
|
+
export const STATUS_CATALOG_NEXT =
|
|
24
|
+
'Add those names to the table on the domain doc (entity · states · allowed from → to). No flag soup.';
|
|
25
|
+
|
|
26
|
+
const SOURCE_EXT = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs', '.cjs']);
|
|
27
|
+
const MAX_FILE_BYTES = 200_000;
|
|
28
|
+
const MAX_ENTITIES = 8;
|
|
29
|
+
const MAX_LITERAL_LEN = 40;
|
|
30
|
+
|
|
31
|
+
function unique(values) {
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const value of values) {
|
|
35
|
+
if (seen.has(value)) continue;
|
|
36
|
+
seen.add(value);
|
|
37
|
+
out.push(value);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isStatusTypeName(name) {
|
|
43
|
+
return name.endsWith('Status') || name.endsWith('State') || name.endsWith('Lifecycle');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function entityFromTypeName(name, fallback) {
|
|
47
|
+
const stripped = name.endsWith('Lifecycle')
|
|
48
|
+
? name.slice(0, -'Lifecycle'.length)
|
|
49
|
+
: name.endsWith('Status')
|
|
50
|
+
? name.slice(0, -'Status'.length)
|
|
51
|
+
: name.endsWith('State')
|
|
52
|
+
? name.slice(0, -'State'.length)
|
|
53
|
+
: name;
|
|
54
|
+
return stripped.length > 0 ? stripped : fallback;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function entityFromFile(rel) {
|
|
58
|
+
const stem = path.basename(rel, path.extname(rel));
|
|
59
|
+
if (!stem) return 'Entity';
|
|
60
|
+
return stem.charAt(0).toUpperCase() + stem.slice(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Quoted string literals only — linear scan, no regex. */
|
|
64
|
+
export function extractQuotedLiterals(text) {
|
|
65
|
+
if (typeof text !== 'string' || text.length === 0) return [];
|
|
66
|
+
const out = [];
|
|
67
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
68
|
+
const ch = text[i];
|
|
69
|
+
if (ch !== "'" && ch !== '"') continue;
|
|
70
|
+
const end = text.indexOf(ch, i + 1);
|
|
71
|
+
if (end < 0) break;
|
|
72
|
+
const lit = text.slice(i + 1, end);
|
|
73
|
+
if (lit.length > 0 && lit.length <= MAX_LITERAL_LEN && !lit.includes('\n')) out.push(lit);
|
|
74
|
+
i = end;
|
|
75
|
+
}
|
|
76
|
+
return unique(out);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readBalanced(text, openIndex, openCh, closeCh) {
|
|
80
|
+
let depth = 0;
|
|
81
|
+
for (let i = openIndex; i < text.length; i += 1) {
|
|
82
|
+
const ch = text[i];
|
|
83
|
+
if (ch === openCh) depth += 1;
|
|
84
|
+
else if (ch === closeCh) {
|
|
85
|
+
depth -= 1;
|
|
86
|
+
if (depth === 0) return text.slice(openIndex + 1, i);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function indexOfKeyword(text, keyword, from) {
|
|
93
|
+
let idx = from;
|
|
94
|
+
while (idx < text.length) {
|
|
95
|
+
const found = text.indexOf(keyword, idx);
|
|
96
|
+
if (found < 0) return -1;
|
|
97
|
+
const prev = found === 0 ? ' ' : text[found - 1];
|
|
98
|
+
if (!/[A-Za-z0-9_]/.test(prev)) return found;
|
|
99
|
+
idx = found + keyword.length;
|
|
100
|
+
}
|
|
101
|
+
return -1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function nextIdent(text, from) {
|
|
105
|
+
let i = from;
|
|
106
|
+
while (i < text.length && /\s/.test(text[i])) i += 1;
|
|
107
|
+
if (i >= text.length || !/[A-Za-z_]/.test(text[i])) return null;
|
|
108
|
+
let end = i + 1;
|
|
109
|
+
while (end < text.length && /[A-Za-z0-9_]/.test(text[end])) end += 1;
|
|
110
|
+
return { name: text.slice(i, end), end };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function collectTypeAlias(text, keywordIndex, fallbackEntity) {
|
|
114
|
+
const afterKw = keywordIndex + 4;
|
|
115
|
+
const ident = nextIdent(text, afterKw);
|
|
116
|
+
if (!ident || !isStatusTypeName(ident.name)) return null;
|
|
117
|
+
let i = ident.end;
|
|
118
|
+
while (i < text.length && /\s/.test(text[i])) i += 1;
|
|
119
|
+
if (text[i] !== '=') return null;
|
|
120
|
+
const rhsEnd = text.indexOf(';', i + 1);
|
|
121
|
+
const rhs = text.slice(i + 1, rhsEnd < 0 ? Math.min(text.length, i + 400) : rhsEnd);
|
|
122
|
+
const states = extractQuotedLiterals(rhs);
|
|
123
|
+
if (states.length < 2) return null;
|
|
124
|
+
return {
|
|
125
|
+
entity: entityFromTypeName(ident.name, fallbackEntity),
|
|
126
|
+
states,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function collectEnum(text, keywordIndex, fallbackEntity) {
|
|
131
|
+
const afterKw = keywordIndex + 4;
|
|
132
|
+
const ident = nextIdent(text, afterKw);
|
|
133
|
+
if (!ident || !isStatusTypeName(ident.name)) return null;
|
|
134
|
+
let i = ident.end;
|
|
135
|
+
while (i < text.length && /\s/.test(text[i])) i += 1;
|
|
136
|
+
if (text[i] !== '{') return null;
|
|
137
|
+
const body = readBalanced(text, i, '{', '}');
|
|
138
|
+
const states = extractQuotedLiterals(body);
|
|
139
|
+
if (states.length < 2) return null;
|
|
140
|
+
return {
|
|
141
|
+
entity: entityFromTypeName(ident.name, fallbackEntity),
|
|
142
|
+
states,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function collectInlineFieldUnions(text) {
|
|
147
|
+
const found = [];
|
|
148
|
+
const needles = ['status:', 'state:', 'status?:', 'state?:'];
|
|
149
|
+
for (const needle of needles) {
|
|
150
|
+
let from = 0;
|
|
151
|
+
while (from < text.length) {
|
|
152
|
+
const idx = text.indexOf(needle, from);
|
|
153
|
+
if (idx < 0) break;
|
|
154
|
+
const prev = idx === 0 ? ' ' : text[idx - 1];
|
|
155
|
+
if (/[A-Za-z0-9_]/.test(prev)) {
|
|
156
|
+
from = idx + needle.length;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const rhsEnd = Math.min(text.length, idx + needle.length + 240);
|
|
160
|
+
const rhs = text.slice(idx + needle.length, rhsEnd);
|
|
161
|
+
const states = extractQuotedLiterals(rhs);
|
|
162
|
+
if (states.length >= 2) found.push(states);
|
|
163
|
+
from = idx + needle.length;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return found;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isIdentBoundary(text, index) {
|
|
170
|
+
if (index < 0 || index >= text.length) return true;
|
|
171
|
+
return !/[A-Za-z0-9_-]/.test(text[index]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function collectUnquotedTransitions(text, allowed) {
|
|
175
|
+
const pairs = [];
|
|
176
|
+
const arrows = [' → ', ' -> ', '→', '->'];
|
|
177
|
+
const states = [...allowed].sort((a, b) => b.length - a.length);
|
|
178
|
+
for (const fromState of states) {
|
|
179
|
+
let from = 0;
|
|
180
|
+
while (from < text.length) {
|
|
181
|
+
const idx = text.indexOf(fromState, from);
|
|
182
|
+
if (idx < 0) break;
|
|
183
|
+
if (!isIdentBoundary(text, idx - 1) || !isIdentBoundary(text, idx + fromState.length)) {
|
|
184
|
+
from = idx + fromState.length;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
let rest = idx + fromState.length;
|
|
188
|
+
while (rest < text.length && /\s/.test(text[rest])) rest += 1;
|
|
189
|
+
let matched = null;
|
|
190
|
+
for (const arrow of arrows) {
|
|
191
|
+
if (text.startsWith(arrow, rest)) {
|
|
192
|
+
matched = arrow;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!matched) {
|
|
197
|
+
from = idx + fromState.length;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
rest += matched.length;
|
|
201
|
+
while (rest < text.length && /\s/.test(text[rest])) rest += 1;
|
|
202
|
+
const toState = states.find(
|
|
203
|
+
(candidate) =>
|
|
204
|
+
text.startsWith(candidate, rest) && isIdentBoundary(text, rest + candidate.length)
|
|
205
|
+
);
|
|
206
|
+
if (toState && toState !== fromState) pairs.push(`${fromState} → ${toState}`);
|
|
207
|
+
from = idx + fromState.length;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return unique(pairs);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function collectQuotedTransitions(text, allowed) {
|
|
214
|
+
if (allowed.size === 0) return [];
|
|
215
|
+
const pairs = collectUnquotedTransitions(text, allowed);
|
|
216
|
+
const arrows = [' → ', ' -> ', '→', '->'];
|
|
217
|
+
for (const quote of ["'", '"']) {
|
|
218
|
+
let from = 0;
|
|
219
|
+
while (from < text.length) {
|
|
220
|
+
const open = text.indexOf(quote, from);
|
|
221
|
+
if (open < 0) break;
|
|
222
|
+
const mid = text.indexOf(quote, open + 1);
|
|
223
|
+
if (mid < 0) break;
|
|
224
|
+
const left = text.slice(open + 1, mid);
|
|
225
|
+
let rest = mid + 1;
|
|
226
|
+
while (rest < text.length && /\s/.test(text[rest])) rest += 1;
|
|
227
|
+
let matched = null;
|
|
228
|
+
for (const arrow of arrows) {
|
|
229
|
+
if (text.startsWith(arrow, rest)) {
|
|
230
|
+
matched = arrow;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (!matched) {
|
|
235
|
+
from = mid + 1;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
rest += matched.length;
|
|
239
|
+
while (rest < text.length && /\s/.test(text[rest])) rest += 1;
|
|
240
|
+
if (text[rest] !== quote) {
|
|
241
|
+
from = mid + 1;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const close = text.indexOf(quote, rest + 1);
|
|
245
|
+
if (close < 0) break;
|
|
246
|
+
const right = text.slice(rest + 1, close);
|
|
247
|
+
if (allowed.has(left) && allowed.has(right) && left !== right) {
|
|
248
|
+
pairs.push(`${left} → ${right}`);
|
|
249
|
+
}
|
|
250
|
+
from = close + 1;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return unique(pairs);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Catalog closed status/state vocabularies in one Domain source file.
|
|
258
|
+
*
|
|
259
|
+
* @param {string} rel
|
|
260
|
+
* @param {string} content
|
|
261
|
+
* @returns {{ entity: string, file: string, states: string[], transitions: string[] }[]}
|
|
262
|
+
*/
|
|
263
|
+
export function extractStatusCatalogFromSource(rel, content) {
|
|
264
|
+
if (typeof content !== 'string' || content.length === 0) return [];
|
|
265
|
+
const fallback = entityFromFile(rel);
|
|
266
|
+
const rows = [];
|
|
267
|
+
let from = 0;
|
|
268
|
+
while (from < content.length) {
|
|
269
|
+
const typeIdx = indexOfKeyword(content, 'type ', from);
|
|
270
|
+
const enumIdx = indexOfKeyword(content, 'enum ', from);
|
|
271
|
+
const next =
|
|
272
|
+
typeIdx < 0 ? enumIdx : enumIdx < 0 ? typeIdx : Math.min(typeIdx, enumIdx);
|
|
273
|
+
if (next < 0) break;
|
|
274
|
+
if (content.startsWith('type ', next)) {
|
|
275
|
+
const row = collectTypeAlias(content, next, fallback);
|
|
276
|
+
if (row) rows.push(row);
|
|
277
|
+
from = next + 5;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (content.startsWith('enum ', next)) {
|
|
281
|
+
const row = collectEnum(content, next, fallback);
|
|
282
|
+
if (row) rows.push(row);
|
|
283
|
+
from = next + 5;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
from = next + 1;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (rows.length === 0) {
|
|
290
|
+
for (const states of collectInlineFieldUnions(content)) {
|
|
291
|
+
rows.push({ entity: fallback, states });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const merged = new Map();
|
|
296
|
+
for (const row of rows) {
|
|
297
|
+
const key = row.entity;
|
|
298
|
+
const prev = merged.get(key);
|
|
299
|
+
if (!prev) {
|
|
300
|
+
merged.set(key, { entity: row.entity, file: rel, states: [...row.states], transitions: [] });
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
prev.states = unique([...prev.states, ...row.states]);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const allowed = new Set();
|
|
307
|
+
for (const row of merged.values()) {
|
|
308
|
+
for (const state of row.states) allowed.add(state);
|
|
309
|
+
}
|
|
310
|
+
const transitions = collectQuotedTransitions(content, allowed);
|
|
311
|
+
for (const row of merged.values()) {
|
|
312
|
+
row.transitions = transitions.filter((pair) => {
|
|
313
|
+
const [fromState, toState] = pair.split(' → ');
|
|
314
|
+
return row.states.includes(fromState) && row.states.includes(toState);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return [...merged.values()];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function isSourceFile(rel) {
|
|
321
|
+
return SOURCE_EXT.has(path.extname(rel).toLowerCase());
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function readSource(abs) {
|
|
325
|
+
try {
|
|
326
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) return null;
|
|
327
|
+
const stat = fs.statSync(abs);
|
|
328
|
+
if (stat.size === 0 || stat.size > MAX_FILE_BYTES) return null;
|
|
329
|
+
return fs.readFileSync(abs, 'utf8');
|
|
330
|
+
} catch {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function domainLayersFromConfig(config) {
|
|
336
|
+
return (config?.layers ?? []).filter((layer) =>
|
|
337
|
+
isDomainRoleLayerName(layer.name, layer.intentPrefixes ?? [])
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Closed catalog from already-classified Domain-role files. No docs walk.
|
|
343
|
+
*
|
|
344
|
+
* @param {{ root?: string, config?: object, files?: readonly string[] }} [input]
|
|
345
|
+
* @returns {{ entity: string, file: string, states: string[], transitions: string[] }[]}
|
|
346
|
+
*/
|
|
347
|
+
export function collectStatusTransitionCatalog(input = {}) {
|
|
348
|
+
const root = input.root ?? '';
|
|
349
|
+
const files = Array.isArray(input.files) ? input.files : [];
|
|
350
|
+
if (typeof root !== 'string' || root.length === 0 || files.length === 0) return [];
|
|
351
|
+
const domainLayers = domainLayersFromConfig(input.config);
|
|
352
|
+
if (domainLayers.length === 0) return [];
|
|
353
|
+
const domainNames = new Set(domainLayers.map((layer) => layer.name));
|
|
354
|
+
const layers = input.config?.layers ?? [];
|
|
355
|
+
const catalog = [];
|
|
356
|
+
for (const file of files) {
|
|
357
|
+
if (typeof file !== 'string' || file.length === 0) continue;
|
|
358
|
+
const abs = path.isAbsolute(file) ? file : path.resolve(root, file);
|
|
359
|
+
const rel = path.relative(root, abs).split(path.sep).join('/');
|
|
360
|
+
if (rel.startsWith('..') || !isSourceFile(rel)) continue;
|
|
361
|
+
const layer = layerForFile(root, abs, layers);
|
|
362
|
+
if (!layer || !domainNames.has(layer)) continue;
|
|
363
|
+
const content = readSource(abs);
|
|
364
|
+
if (content == null) continue;
|
|
365
|
+
for (const row of extractStatusCatalogFromSource(rel, content)) {
|
|
366
|
+
catalog.push(row);
|
|
367
|
+
if (catalog.length >= MAX_ENTITIES) return catalog;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return catalog;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function formatEntityList(entities) {
|
|
374
|
+
const parts = entities.map((row) => `${row.entity} (${row.states.join(', ')})`);
|
|
375
|
+
if (parts.length === 1) return parts[0];
|
|
376
|
+
if (parts.length === 2) return `${parts[0]} and ${parts[1]}`;
|
|
377
|
+
return `${parts.slice(0, -1).join(', ')}, and ${parts[parts.length - 1]}`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function nextActionFor(home, entities) {
|
|
381
|
+
const names = entities.map((row) => row.entity).join(', ');
|
|
382
|
+
const target = home || 'the domain doc';
|
|
383
|
+
return `Add ${names} to the table on ${target} (entity · states · allowed from → to). Names from the code. No flag soup.`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Soft residual when Domain already has a status catalog and the domain-doc
|
|
388
|
+
* map is missing or heading-only. Silent when the catalog is empty, the
|
|
389
|
+
* domain doc is absent, or the map is already present.
|
|
390
|
+
*
|
|
391
|
+
* @param {{ root?: string, config?: object, files?: readonly string[], statesTransitions?: { kind?: string, home?: string } | null }} [input]
|
|
392
|
+
* @returns {{ kind: 'thin', home: string, entities: object[], ask: string, nextAction: string } | null}
|
|
393
|
+
*/
|
|
394
|
+
export function collectStatusTransitionCatalogResidual(input = {}) {
|
|
395
|
+
const catalog = collectStatusTransitionCatalog(input);
|
|
396
|
+
if (catalog.length === 0) return null;
|
|
397
|
+
const map =
|
|
398
|
+
input.statesTransitions === undefined
|
|
399
|
+
? collectStatesTransitionsResidual({ root: input.root ?? '' })
|
|
400
|
+
: input.statesTransitions;
|
|
401
|
+
if (!map || (map.kind !== 'missing' && map.kind !== 'incomplete')) return null;
|
|
402
|
+
const listed = catalog.slice(0, MAX_ENTITIES);
|
|
403
|
+
return {
|
|
404
|
+
kind: 'thin',
|
|
405
|
+
home: map.home,
|
|
406
|
+
entities: listed,
|
|
407
|
+
ask: `Domain code already names ${formatEntityList(listed)}, but the states → transitions map is still thin.`,
|
|
408
|
+
nextAction: nextActionFor(map.home, listed),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
@@ -235,6 +235,16 @@ export function runTeamPreflight({ root, args, config, policyDelta, teamBase })
|
|
|
235
235
|
return { halt: null, teamParliament: null, changedPaths: [] };
|
|
236
236
|
}
|
|
237
237
|
const againstRef = args.against || teamBase;
|
|
238
|
+
if (args.local && !againstRef) {
|
|
239
|
+
const message =
|
|
240
|
+
'--local needs a git merge base so it can reuse --changed. Pass --base <ref> (for example --base HEAD or --base origin/main). The merge gate stays --strict-merge.';
|
|
241
|
+
const teamParliament = { deny: false, reasonId: 'local-needs-base', message };
|
|
242
|
+
return {
|
|
243
|
+
halt: { exitCode: 2, message, teamParliament },
|
|
244
|
+
teamParliament,
|
|
245
|
+
changedPaths: [],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
238
248
|
const listed = againstRef ? listChangedPaths(root, againstRef) : { ok: true, paths: [], error: null };
|
|
239
249
|
const changedPaths = listed.ok ? listed.paths : [];
|
|
240
250
|
const changeSet = classifyChangeSet(changedPaths);
|
package/bin/lib/violations.mjs
CHANGED
|
@@ -97,10 +97,22 @@ export function printWarning(warning) {
|
|
|
97
97
|
);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/** Non-blocking type-only placement debt: exit stays 0; do not paint ✖. */
|
|
101
|
+
export function isNonBlockingPlacementDebt(violation) {
|
|
102
|
+
return violation?.failsStrict === false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Human mark for one finding — ⚠ when the run still passes, ✖ when it fails. */
|
|
106
|
+
export function violationMark(violation) {
|
|
107
|
+
return isNonBlockingPlacementDebt(violation) ? '⚠' : '✖';
|
|
108
|
+
}
|
|
109
|
+
|
|
100
110
|
export function printViolation(violation) {
|
|
101
111
|
const location = `${violation.file}:${violation.line}`;
|
|
102
112
|
const plane = violationPlaneLabel(violation.ruleId);
|
|
103
|
-
|
|
113
|
+
const mark = violationMark(violation);
|
|
114
|
+
const painted = isNonBlockingPlacementDebt(violation) ? color.yellow(mark) : color.red(mark);
|
|
115
|
+
console.error(`${painted} ${color.bold(`${plane}${violation.ruleId}`)} ${location}`);
|
|
104
116
|
if (violation.fromLayer && violation.toLayer) {
|
|
105
117
|
const target = violation.target ? ` ${color.dim(`(${violation.target})`)}` : '';
|
|
106
118
|
console.error(` ${violation.fromLayer} → ${violation.toLayer}${target}`);
|
|
@@ -313,14 +313,20 @@ function cursorHookEvidence(root) {
|
|
|
313
313
|
hooks = [];
|
|
314
314
|
}
|
|
315
315
|
const hardHooks = hooks.filter((entry) => entry.invocation.binArgs.includes('--hook'));
|
|
316
|
+
// Cursor failClosed is the host-native FS analog: if the checker cannot run, deny.
|
|
317
|
+
// A Write|StrReplace Ark hook without failClosed:true is configured but fail-open.
|
|
318
|
+
const failClosedHooks = hardHooks.filter((entry) => entry.hook?.failClosed === true);
|
|
316
319
|
const required = requiredWriteOperations(relativePath);
|
|
317
|
-
const
|
|
320
|
+
const configuredArk = required.every((operation) =>
|
|
318
321
|
hardHooks.some((entry) => entry.operations.includes(operation))
|
|
319
322
|
);
|
|
323
|
+
const hard = required.every((operation) =>
|
|
324
|
+
failClosedHooks.some((entry) => entry.operations.includes(operation))
|
|
325
|
+
);
|
|
320
326
|
const repair =
|
|
321
327
|
hard &&
|
|
322
328
|
required.every((operation) =>
|
|
323
|
-
|
|
329
|
+
failClosedHooks.some(
|
|
324
330
|
({ hook, invocation, operations }) =>
|
|
325
331
|
operations.includes(operation) &&
|
|
326
332
|
(invocation.binArgs.includes('--hook-repair') ||
|
|
@@ -332,6 +338,7 @@ function cursorHookEvidence(root) {
|
|
|
332
338
|
return {
|
|
333
339
|
hard: hard ? [relativePath] : [],
|
|
334
340
|
repair: repair ? [relativePath] : [],
|
|
341
|
+
failOpen: configuredArk && !hard,
|
|
335
342
|
};
|
|
336
343
|
}
|
|
337
344
|
|
|
@@ -424,7 +431,7 @@ function codexMcpEvidence(root) {
|
|
|
424
431
|
return registered ? [relativeEvidencePath(root, file)] : [];
|
|
425
432
|
}
|
|
426
433
|
|
|
427
|
-
function hostRecord(hard, advisory, repair, merge) {
|
|
434
|
+
function hostRecord(hard, advisory, repair, merge, extras = {}) {
|
|
428
435
|
const evidence = {
|
|
429
436
|
'hard-write': unique(hard),
|
|
430
437
|
'advisory-write': unique(advisory),
|
|
@@ -434,9 +441,11 @@ function hostRecord(hard, advisory, repair, merge) {
|
|
|
434
441
|
return {
|
|
435
442
|
configured:
|
|
436
443
|
evidence['hard-write'].length > 0 ||
|
|
437
|
-
evidence['advisory-write'].length > 0
|
|
444
|
+
evidence['advisory-write'].length > 0 ||
|
|
445
|
+
extras.nativeFailClosed === false,
|
|
438
446
|
capabilities: capabilityMap(evidence),
|
|
439
447
|
evidence,
|
|
448
|
+
nativeFailClosed: extras.nativeFailClosed ?? null,
|
|
440
449
|
};
|
|
441
450
|
}
|
|
442
451
|
|
|
@@ -480,7 +489,11 @@ export function detectWritePathInventory(root) {
|
|
|
480
489
|
// reinjection is not package-guaranteed — keep inventory repair-payload false
|
|
481
490
|
// (envelope honesty lives on support.repair-envelope-emitted).
|
|
482
491
|
[],
|
|
483
|
-
merge
|
|
492
|
+
merge,
|
|
493
|
+
{
|
|
494
|
+
nativeFailClosed:
|
|
495
|
+
cursorHook.hard.length > 0 ? true : cursorHook.failOpen ? false : null,
|
|
496
|
+
}
|
|
484
497
|
),
|
|
485
498
|
// Current Codex CLI and local ChatGPT Desktop/App Server synchronously run
|
|
486
499
|
// PreToolUse for apply_patch. Disk evidence remains unverified until a fresh
|
|
@@ -542,6 +555,8 @@ export function buildWritePathCapabilityModel(root, explicitHost, attempt) {
|
|
|
542
555
|
};
|
|
543
556
|
model.enforcementState = buildEnforcementState(root, { ...model, ci });
|
|
544
557
|
model.enforcementLadder.ciMerge.requiredStatus = model.enforcementState.ciMerge.required;
|
|
558
|
+
model.nativeFailClosed = activeRecord?.nativeFailClosed ?? null;
|
|
559
|
+
model.nativeFailClosedPolicy = getHostSupportProfile(activeHost)?.nativeFailClosed ?? 'unsupported';
|
|
545
560
|
// Ladder/state agreement stamp for pin-absent consumers (configured ≠ installed).
|
|
546
561
|
if (pkg.installed !== true) {
|
|
547
562
|
model.packagePinHonesty = {
|
|
@@ -54,7 +54,17 @@ export function buildUnknownHostSessionNote(inventory, capabilities = {}) {
|
|
|
54
54
|
|
|
55
55
|
export function detectWritePathCapabilities(root, explicitHost, attempt) {
|
|
56
56
|
const model = buildWritePathCapabilityModel(root, explicitHost, attempt);
|
|
57
|
-
const {
|
|
57
|
+
const {
|
|
58
|
+
activeHost,
|
|
59
|
+
support,
|
|
60
|
+
capabilities,
|
|
61
|
+
capabilityEvidence,
|
|
62
|
+
enforcementLadder,
|
|
63
|
+
enforcementState,
|
|
64
|
+
inventory,
|
|
65
|
+
nativeFailClosed,
|
|
66
|
+
nativeFailClosedPolicy,
|
|
67
|
+
} = model;
|
|
58
68
|
const hardWrite = capabilities['hard-write'];
|
|
59
69
|
const advisoryWrite = capabilities['advisory-write'];
|
|
60
70
|
const repairPayload = capabilities['repair-payload'];
|
|
@@ -74,7 +84,20 @@ export function detectWritePathCapabilities(root, explicitHost, attempt) {
|
|
|
74
84
|
const inventoryHasWriteBoundary =
|
|
75
85
|
Boolean(inventory?.capabilities?.['hard-write']) ||
|
|
76
86
|
Boolean(inventory?.capabilities?.['advisory-write']);
|
|
77
|
-
if (
|
|
87
|
+
if (nativeFailClosed === false) {
|
|
88
|
+
const hookPath = support?.hookPath || 'the write hook';
|
|
89
|
+
const label = support?.label || activeHost;
|
|
90
|
+
gap = {
|
|
91
|
+
id: 'write-path-fail-open',
|
|
92
|
+
severity: 'warn',
|
|
93
|
+
host: activeHost,
|
|
94
|
+
message:
|
|
95
|
+
`${label}: the write hook is fail-open. If the checker cannot run, the write still lands. ` +
|
|
96
|
+
`Set failClosed: true on ${hookPath} — same idea as a file permission: no checker, no write. ` +
|
|
97
|
+
'Required CI is the shared merge line.',
|
|
98
|
+
fix: arkCommand(root, 'ark-check', `--install-agent-gates --tools ${tools} --force`),
|
|
99
|
+
};
|
|
100
|
+
} else if (mode === 'none') {
|
|
78
101
|
if (activeHost === 'unknown' && inventoryHasWriteBoundary) {
|
|
79
102
|
gap = null;
|
|
80
103
|
} else {
|
|
@@ -150,6 +173,8 @@ export function detectWritePathCapabilities(root, explicitHost, attempt) {
|
|
|
150
173
|
inventory,
|
|
151
174
|
// Configured inventory (on-disk hosts) vs this-invocation projection (activeHost).
|
|
152
175
|
...(sessionNote ? { sessionNote } : {}),
|
|
176
|
+
nativeFailClosed: nativeFailClosed ?? null,
|
|
177
|
+
nativeFailClosedPolicy: nativeFailClosedPolicy ?? 'unsupported',
|
|
153
178
|
// Compatibility projection for existing doctor/API consumers.
|
|
154
179
|
mode,
|
|
155
180
|
prepareWrite: advisoryWrite,
|
|
@@ -11,6 +11,8 @@ type ArkConfigCyclePolicy = 'strict' | 'soft' | 'framework-soft' | 'off';
|
|
|
11
11
|
type ArkConfigLayerCapabilities = {
|
|
12
12
|
deny?: string[];
|
|
13
13
|
};
|
|
14
|
+
/** Who this folder is for. Metadata — not import-rule teeth. Absence is silent. */
|
|
15
|
+
type ArkConfigLayerTrustBoundary = 'public' | 'auth' | 'admin' | 'internal';
|
|
14
16
|
type ArkConfigLayer = {
|
|
15
17
|
name: string;
|
|
16
18
|
patterns: string[];
|
|
@@ -21,6 +23,17 @@ type ArkConfigLayer = {
|
|
|
21
23
|
* Metadata — excluded from policy hash. Absence is silent.
|
|
22
24
|
*/
|
|
23
25
|
description?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Who this folder is for: public | auth | admin | internal.
|
|
28
|
+
* Metadata — excluded from policy hash. Absence is silent. Not host/CI TLS.
|
|
29
|
+
*/
|
|
30
|
+
trustBoundary?: ArkConfigLayerTrustBoundary;
|
|
31
|
+
/**
|
|
32
|
+
* Who owns this house: GitHub handles or emails (same identity as stewards).
|
|
33
|
+
* Metadata — excluded from policy hash. Absence is silent unless
|
|
34
|
+
* `requireLayerOwners` is true. Not import-rule teeth.
|
|
35
|
+
*/
|
|
36
|
+
owners?: string[];
|
|
24
37
|
forbiddenGlobals?: string[];
|
|
25
38
|
/** ADR 0009 D2 — opt-in effect-capability walls; absence changes no verdict. */
|
|
26
39
|
capabilities?: ArkConfigLayerCapabilities;
|
|
@@ -153,6 +166,12 @@ type ArkConfig = {
|
|
|
153
166
|
* Metadata — excluded from policy hash. Absence means no steward lock (policy-ack still applies).
|
|
154
167
|
*/
|
|
155
168
|
stewards?: string[];
|
|
169
|
+
/**
|
|
170
|
+
* When true, every non-reserved layer must name `owners`. Absence/false is
|
|
171
|
+
* silent. This flag is policy teeth (stays in policyHash). Owners themselves
|
|
172
|
+
* stay metadata.
|
|
173
|
+
*/
|
|
174
|
+
requireLayerOwners?: boolean;
|
|
156
175
|
};
|
|
157
176
|
type ArkConfigIssue = {
|
|
158
177
|
path: 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-CN9tVMPz.js';
|
|
2
|
+
import { A as ArkConfig, c as ArkConfigLoadResult } from './configTypes-Dt3DpVbd.js';
|
|
3
3
|
|
|
4
4
|
/** Versioned public result contract types + JSON Schema (adapter envelope). */
|
|
5
5
|
/**
|
|
@@ -392,7 +392,7 @@ declare function createAdapterResult(input: {
|
|
|
392
392
|
}): CurrentAdapterResult;
|
|
393
393
|
|
|
394
394
|
/** ArkGate library version — single source of truth. */
|
|
395
|
-
declare const version = "4.8.
|
|
395
|
+
declare const version = "4.8.15";
|
|
396
396
|
|
|
397
397
|
/**
|
|
398
398
|
* AI Code Gate (basic).
|