arkgate 4.8.24 → 4.8.26
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 +95 -0
- package/README.md +2 -2
- package/bin/ark-check-runtime.mjs +17 -14
- package/bin/ark-layer-match.mjs +115 -13
- package/bin/ark-mcp-runtime.mjs +36 -31
- package/bin/ark-shared.mjs +4 -23
- package/bin/lib/adoption-stance.mjs +2 -2
- package/bin/lib/adr-path.mjs +3 -3
- package/bin/lib/agent-gates.mjs +0 -61
- package/bin/lib/agent-homes.mjs +12 -21
- package/bin/lib/agent-projection-command.mjs +4 -4
- package/bin/lib/ambient-state.mjs +9 -30
- package/bin/lib/analysis-engine.mjs +14 -14
- package/bin/lib/architecture-scan.mjs +11 -9
- package/bin/lib/ark-order-doctor.mjs +3 -28
- package/bin/lib/ark-order-facts.mjs +6 -99
- package/bin/lib/ark-order-types.mjs +1 -2
- package/bin/lib/ark-run-doctor.mjs +3 -28
- package/bin/lib/ark-run-facts.mjs +30 -40
- package/bin/lib/ark-run-sensors.mjs +1 -1
- package/bin/lib/arkrule-file-hints.mjs +2 -47
- package/bin/lib/arkrules-contract.mjs +124 -133
- package/bin/lib/arkrules-sensors.mjs +7 -5
- package/bin/lib/check-args.mjs +63 -1
- package/bin/lib/ci-and-commands.mjs +8 -72
- package/bin/lib/ci-merge-boundary.mjs +1 -1
- package/bin/lib/class-source-scan.mjs +5 -5
- package/bin/lib/clone-detection.mjs +474 -0
- package/bin/lib/codex-home.mjs +1 -1
- package/bin/lib/config-contract-slices.mjs +44 -6
- package/bin/lib/config-contract.mjs +40 -117
- package/bin/lib/config-extras.mjs +16 -22
- package/bin/lib/contract-smells.mjs +33 -15
- package/bin/lib/core-ratchet.mjs +0 -2
- package/bin/lib/dashboard-cli.mjs +2 -2
- package/bin/lib/deep-module-coach.mjs +2 -4
- package/bin/lib/deploy-path.mjs +13 -38
- package/bin/lib/design-smells.mjs +2 -2
- package/bin/lib/diagnostic-catalog.mjs +8 -1
- package/bin/lib/doctor-advisories.mjs +55 -9
- package/bin/lib/doctor-green-cite.mjs +1 -1
- package/bin/lib/doctor-human.mjs +1 -1
- package/bin/lib/doctor-next-actions.mjs +2 -2
- package/bin/lib/doctor-plan.mjs +4 -6
- package/bin/lib/duplication-io.mjs +572 -0
- package/bin/lib/effective-contract-load.mjs +181 -44
- package/bin/lib/entry-points-io.mjs +424 -0
- package/bin/lib/extra-merge-teeth.mjs +36 -0
- package/bin/lib/field-install.mjs +38 -14
- package/bin/lib/first-run-help.mjs +7 -3
- package/bin/lib/flat-parent-importers.mjs +5 -3
- package/bin/lib/gate-files.mjs +30 -89
- package/bin/lib/github-enforcement.mjs +165 -28
- package/bin/lib/golden-pattern.mjs +1 -1
- package/bin/lib/hook-templates.mjs +16 -16
- package/bin/lib/html-report-advisories.mjs +4 -0
- package/bin/lib/html-report-depth.mjs +2 -2
- package/bin/lib/html-report.mjs +2 -2
- package/bin/lib/import-graph-projection.mjs +112 -0
- package/bin/lib/improvement-compass-doctor.mjs +1 -24
- package/bin/lib/invariant-coverage-io.mjs +4 -3
- package/bin/lib/invariant-coverage.mjs +26 -0
- package/bin/lib/invariant-probe-cli.mjs +347 -0
- package/bin/lib/invariant-probe-io.mjs +165 -0
- package/bin/lib/invariant-probe-runner.mjs +268 -0
- package/bin/lib/invariant-probe-sites.mjs +174 -0
- package/bin/lib/invariant-probe-workspace.mjs +183 -0
- package/bin/lib/invariant-probe.mjs +461 -0
- package/bin/lib/invariant-tests-path.mjs +16 -22
- package/bin/lib/literal-path-drift.mjs +4 -2
- package/bin/lib/managed-upgrade.mjs +2 -8
- package/bin/lib/mcp-adoption.mjs +4 -5
- package/bin/lib/mcp-hook-payload.mjs +4 -4
- package/bin/lib/orphan-modules-io.mjs +458 -0
- package/bin/lib/orphan-modules.mjs +251 -0
- package/bin/lib/outside-importers.mjs +197 -0
- package/bin/lib/package-manager.mjs +3 -3
- package/bin/lib/parse-health.mjs +1 -1
- package/bin/lib/persistence-write-hint.mjs +33 -8
- package/bin/lib/pin-dependent-gates.mjs +2 -2
- package/bin/lib/policy-delta-io.mjs +60 -12
- package/bin/lib/post-green-path.mjs +1 -1
- package/bin/lib/prepare-change.mjs +3 -9
- package/bin/lib/prepare-write.mjs +1 -2
- package/bin/lib/presets.mjs +56 -68
- package/bin/lib/product-copy.mjs +1 -6
- package/bin/lib/project-root.mjs +43 -85
- package/bin/lib/resolved-candidate-facts.mjs +90 -62
- package/bin/lib/rules-under-contract.mjs +72 -8
- package/bin/lib/scan-files.mjs +4 -4
- package/bin/lib/schema-validation.mjs +130 -0
- package/bin/lib/sensor-promote-cli.mjs +2 -11
- package/bin/lib/sensor-promote-io.mjs +33 -22
- package/bin/lib/sensor-promotion.mjs +1 -1
- package/bin/lib/skill-catalog-apply.mjs +1 -1
- package/bin/lib/skill-install.mjs +42 -38
- package/bin/lib/snippet-analysis.mjs +1 -1
- package/bin/lib/start-install-recovery.mjs +1 -1
- package/bin/lib/start-preview.mjs +1 -1
- package/bin/lib/states-transitions-presence.mjs +3 -3
- package/bin/lib/status-command.mjs +2 -18
- package/bin/lib/status-manifest.mjs +18 -26
- package/bin/lib/status-transition-catalog.mjs +0 -6
- package/bin/lib/suggestions.mjs +3 -3
- package/bin/lib/team-parliament-io.mjs +3 -17
- package/bin/lib/team-parliament.mjs +15 -9
- package/bin/lib/upgrade-behavior-changes.mjs +2 -2
- package/bin/lib/upgrade-command.mjs +2 -24
- package/bin/lib/upgrade-outcome.mjs +1 -1
- package/bin/lib/upstream-report.mjs +21 -10
- package/bin/lib/violations.mjs +10 -9
- package/bin/lib/weakest-link.mjs +13 -0
- package/bin/lib/write-path-capabilities.mjs +2 -2
- package/dist/chunk-G3JRBJGH.js +16 -0
- package/dist/chunk-R3AJ5RSZ.cjs +16 -0
- package/dist/{configTypes-BJbaeoj4.d.cts → configTypes-GPvvdleM.d.cts} +12 -1
- package/dist/{configTypes-BJbaeoj4.d.ts → configTypes-GPvvdleM.d.ts} +12 -1
- package/dist/eslint/index.cjs +11 -11
- package/dist/eslint/index.d.cts +10 -1
- package/dist/eslint/index.d.ts +10 -1
- package/dist/eslint/index.js +11 -11
- package/dist/index.cjs +34 -34
- package/dist/index.d.cts +587 -515
- package/dist/index.d.ts +587 -515
- package/dist/index.js +34 -34
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.d.cts +1 -1
- package/dist/nestjs/index.d.ts +1 -1
- package/dist/nestjs/index.js +1 -1
- package/dist/order/index.cjs +1 -1
- package/dist/order/index.js +1 -1
- package/dist/runtime/index.cjs +12 -12
- package/dist/runtime/index.d.cts +174 -136
- package/dist/runtime/index.d.ts +174 -136
- package/dist/runtime/index.js +12 -12
- package/dist/{types-DRinG-vT.d.cts → types-CnauMQQL.d.cts} +33 -25
- package/dist/{types-DRinG-vT.d.ts → types-CnauMQQL.d.ts} +33 -25
- package/docs/README.md +3 -1
- package/docs/agent-guide.md +140 -6
- package/docs/ai-gates.md +15 -1
- package/docs/configuration.md +40 -11
- package/docs/diagnostics.md +119 -3
- package/docs/package-surface.md +4 -1
- package/docs/product-voice.md +7 -1
- package/package.json +11 -2
- package/schemas/ark.config.schema.json +31 -3
- package/schemas/ark.invariant-probe.schema.json +368 -0
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +1 -0
- package/templates/agent-skills/ark-contract/SKILL.md +17 -0
- package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
- package/templates/agent-skills/ark-explore/SKILL.md +7 -0
- package/templates/agent-skills/ark-fix/SKILL.md +20 -0
- package/templates/agent-skills/ark-place/SKILL.md +8 -0
- package/templates/skills/ark-adopt.md +1 -0
- package/templates/skills/ark-contract.md +17 -0
- package/templates/skills/ark-coverage.md +2 -0
- package/templates/skills/ark-explore.md +7 -0
- package/templates/skills/ark-fix.md +20 -0
- package/templates/skills/ark-place.md +8 -0
- package/bin/lib/ark-order-error.mjs +0 -18
- package/bin/lib/ark-order-invariants.mjs +0 -388
- package/bin/lib/stable-hash.mjs +0 -90
- package/dist/chunk-MBXCFLO4.js +0 -16
- package/dist/chunk-RNQHRDCQ.cjs +0 -16
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* Pure CLI helper (bin/lib/arkrules-contract.mjs). Zero Node I/O.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { isObject, validateSchemaNode, valueType } from './schema-validation.mjs';
|
|
11
12
|
export const ARK_RULES_SCHEMA_VERSION = '1.0';
|
|
12
|
-
|
|
13
|
+
const ARK_RULES_SCHEMA_URL = 'https://unpkg.com/arkgate/schemas/ark.arkrules.schema.json';
|
|
13
14
|
/** Closed sensor vocabulary — keep in lockstep with arkRulesTypes.ARK_RULE_SENSOR_IDS. */
|
|
14
15
|
export const ARK_RULE_SENSORS = [
|
|
15
16
|
'aggregate-private-state',
|
|
@@ -105,113 +106,6 @@ export class ArkRulesValidationError extends Error {
|
|
|
105
106
|
this.issues = issues;
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
|
-
function isObject(value) {
|
|
109
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
110
|
-
}
|
|
111
|
-
function propertyPath(parent, key) {
|
|
112
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
|
|
113
|
-
? `${parent}.${key}`
|
|
114
|
-
: `${parent}[${JSON.stringify(key)}]`;
|
|
115
|
-
}
|
|
116
|
-
function valueType(value) {
|
|
117
|
-
if (value === null)
|
|
118
|
-
return 'null';
|
|
119
|
-
if (Array.isArray(value))
|
|
120
|
-
return 'array';
|
|
121
|
-
return typeof value;
|
|
122
|
-
}
|
|
123
|
-
function resolveSchemaRef(ref, root) {
|
|
124
|
-
const prefix = '#/$defs/';
|
|
125
|
-
if (!ref.startsWith(prefix))
|
|
126
|
-
return undefined;
|
|
127
|
-
return root.$defs[ref.slice(prefix.length)];
|
|
128
|
-
}
|
|
129
|
-
function validateNode(value, schema, path, root, issues) {
|
|
130
|
-
if (schema.$ref) {
|
|
131
|
-
const referenced = resolveSchemaRef(schema.$ref, root);
|
|
132
|
-
if (!referenced) {
|
|
133
|
-
issues.push({ path, message: `schema reference ${schema.$ref} cannot be resolved` });
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
validateNode(value, referenced, path, root, issues);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (schema.const !== undefined && !Object.is(value, schema.const)) {
|
|
140
|
-
issues.push({ path, message: `must equal ${JSON.stringify(schema.const)}` });
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
if (schema.enum && !schema.enum.some((candidate) => Object.is(candidate, value))) {
|
|
144
|
-
issues.push({ path, message: `must be one of ${schema.enum.map(String).join(', ')}` });
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
if (schema.type === 'object') {
|
|
148
|
-
if (!isObject(value)) {
|
|
149
|
-
issues.push({ path, message: `must be an object; received ${valueType(value)}` });
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
const properties = schema.properties ?? {};
|
|
153
|
-
for (const key of schema.required ?? []) {
|
|
154
|
-
if (value[key] === undefined) {
|
|
155
|
-
issues.push({ path: propertyPath(path, key), message: 'is required' });
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
if (schema.additionalProperties === false) {
|
|
159
|
-
for (const key of Object.keys(value)) {
|
|
160
|
-
if (!(key in properties)) {
|
|
161
|
-
issues.push({ path: propertyPath(path, key), message: 'unknown field' });
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
else if (isObject(schema.additionalProperties)) {
|
|
166
|
-
const additional = schema.additionalProperties;
|
|
167
|
-
for (const key of Object.keys(value)) {
|
|
168
|
-
if (!(key in properties)) {
|
|
169
|
-
validateNode(value[key], additional, propertyPath(path, key), root, issues);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
for (const [key, childSchema] of Object.entries(properties)) {
|
|
174
|
-
if (value[key] !== undefined) {
|
|
175
|
-
validateNode(value[key], childSchema, propertyPath(path, key), root, issues);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
if (schema.type === 'array') {
|
|
181
|
-
if (!Array.isArray(value)) {
|
|
182
|
-
issues.push({ path, message: `must be an array; received ${valueType(value)}` });
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
186
|
-
issues.push({ path, message: `must contain at least ${schema.minItems} item(s)` });
|
|
187
|
-
}
|
|
188
|
-
if (schema.uniqueItems) {
|
|
189
|
-
const serialized = value.map((entry) => JSON.stringify(entry));
|
|
190
|
-
if (new Set(serialized).size !== serialized.length) {
|
|
191
|
-
issues.push({ path, message: 'must not contain duplicate items' });
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
if (schema.items) {
|
|
195
|
-
value.forEach((entry, index) => validateNode(entry, schema.items, `${path}[${index}]`, root, issues));
|
|
196
|
-
}
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
if (schema.type === 'string') {
|
|
200
|
-
if (typeof value !== 'string') {
|
|
201
|
-
issues.push({ path, message: `must be a string; received ${valueType(value)}` });
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
205
|
-
issues.push({ path, message: `must contain at least ${schema.minLength} character(s)` });
|
|
206
|
-
}
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
if (schema.type === 'boolean') {
|
|
210
|
-
if (typeof value !== 'boolean') {
|
|
211
|
-
issues.push({ path, message: `must be a boolean; received ${valueType(value)}` });
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
109
|
function defaultedArkRules(input) {
|
|
216
110
|
return {
|
|
217
111
|
...input,
|
|
@@ -299,7 +193,7 @@ export function loadArkRulesContract(input, source = 'arkrules.json', expectedLa
|
|
|
299
193
|
}
|
|
300
194
|
const candidate = defaultedArkRules(input);
|
|
301
195
|
const issues = [];
|
|
302
|
-
|
|
196
|
+
validateSchemaNode(candidate, ARK_RULES_SCHEMA, '$', ARK_RULES_SCHEMA, issues);
|
|
303
197
|
validateSemantics(candidate, issues);
|
|
304
198
|
if (expectedLayer !== undefined &&
|
|
305
199
|
typeof candidate.layer === 'string' &&
|
|
@@ -328,39 +222,136 @@ export function parseArkRulesJson(json, source = 'arkrules.json', expectedLayer)
|
|
|
328
222
|
}
|
|
329
223
|
return loadArkRulesContract(input, source, expectedLayer);
|
|
330
224
|
}
|
|
225
|
+
function effectiveRuleId(part, localId) {
|
|
226
|
+
return part.childId ? `${part.childId}#${localId}` : localId;
|
|
227
|
+
}
|
|
228
|
+
function effectiveAppliesTo(part, appliesTo) {
|
|
229
|
+
if (appliesTo && appliesTo.length > 0)
|
|
230
|
+
return [...appliesTo];
|
|
231
|
+
if (part.childId && part.defaultAppliesTo && part.defaultAppliesTo.length > 0) {
|
|
232
|
+
return [...part.defaultAppliesTo];
|
|
233
|
+
}
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
function provenanceFor(part, localId) {
|
|
237
|
+
return {
|
|
238
|
+
sourceFile: part.sourceFile,
|
|
239
|
+
ruleId: effectiveRuleId(part, localId),
|
|
240
|
+
layer: part.layer,
|
|
241
|
+
...(part.childId ? { childId: part.childId } : {}),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* True when every path the glob can match stays under the slice root.
|
|
246
|
+
* A wider prefix, a `**` that is not the last segment, or `..` escapes.
|
|
247
|
+
*/
|
|
248
|
+
function appliesToPatternInsideSlice(pattern, sliceRoot) {
|
|
249
|
+
const root = sliceRoot.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
250
|
+
const value = pattern.trim().replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase();
|
|
251
|
+
if (!value || !root || value.includes('..'))
|
|
252
|
+
return false;
|
|
253
|
+
if (value === root || value === `${root}/**` || value === `${root}/*`)
|
|
254
|
+
return true;
|
|
255
|
+
if (value.startsWith(`${root}/`)) {
|
|
256
|
+
const parts = value.slice(root.length + 1).split('/');
|
|
257
|
+
const globStar = parts.indexOf('**');
|
|
258
|
+
if (globStar !== -1 && globStar !== parts.length - 1)
|
|
259
|
+
return false;
|
|
260
|
+
return parts.every((part) => part.length > 0);
|
|
261
|
+
}
|
|
262
|
+
const bareRoot = root.replace(/^(?:src|app)\//, '');
|
|
263
|
+
const bareValue = value.replace(/^(?:src|app)\//, '');
|
|
264
|
+
if (bareRoot !== root || bareValue !== value)
|
|
265
|
+
return appliesToPatternInsideSlice(bareValue, bareRoot);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
/** Explicit appliesTo on a discovered file that can match outside its slice. */
|
|
269
|
+
export function sliceScopeEscapes(part) {
|
|
270
|
+
if (!part.childId || !part.defaultAppliesTo?.length)
|
|
271
|
+
return [];
|
|
272
|
+
const sliceRoot = String(part.defaultAppliesTo[0]).replace(/\/\*\*$/, '');
|
|
273
|
+
const escapes = [];
|
|
274
|
+
const entries = [...(part.file.structure ?? []), ...(part.file.invariants ?? [])];
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
for (const pattern of entry.appliesTo ?? []) {
|
|
277
|
+
if (!appliesToPatternInsideSlice(pattern, sliceRoot))
|
|
278
|
+
escapes.push({ id: entry.id, pattern });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return escapes;
|
|
282
|
+
}
|
|
283
|
+
/** Effective ids declared by more than one part. Central ids stay bare; slice ids are namespaced. */
|
|
284
|
+
export function duplicateArkRuleIds(parts) {
|
|
285
|
+
const seen = new Map();
|
|
286
|
+
const dupes = new Map();
|
|
287
|
+
for (const part of parts) {
|
|
288
|
+
const localIds = [
|
|
289
|
+
...(part.file.structure ?? []).map((entry) => entry.id),
|
|
290
|
+
...(part.file.invariants ?? []).map((entry) => entry.id),
|
|
291
|
+
];
|
|
292
|
+
for (const localId of localIds) {
|
|
293
|
+
const id = effectiveRuleId(part, localId);
|
|
294
|
+
const key = `${part.layer}\0${id}`;
|
|
295
|
+
const existing = seen.get(key);
|
|
296
|
+
if (!existing) {
|
|
297
|
+
seen.set(key, { id, layer: part.layer, sourceFiles: [part.sourceFile] });
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const row = dupes.get(key) ?? { id, layer: part.layer, sourceFiles: [...existing.sourceFiles] };
|
|
301
|
+
if (!row.sourceFiles.includes(part.sourceFile))
|
|
302
|
+
row.sourceFiles.push(part.sourceFile);
|
|
303
|
+
dupes.set(key, row);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return [...dupes.values()];
|
|
307
|
+
}
|
|
331
308
|
/**
|
|
332
309
|
* Build the Effective Contract from already-validated ArkRules files.
|
|
333
|
-
*
|
|
310
|
+
* Same-layer parts merge. A child id namespaces the effective id and does not rewrite the file.
|
|
334
311
|
*/
|
|
335
312
|
export function buildEffectiveArkRules(parts) {
|
|
336
313
|
const byLayer = {};
|
|
337
314
|
const structure = [];
|
|
338
315
|
const invariants = [];
|
|
339
|
-
const ordered = [...parts].sort((a, b) => a.layer.localeCompare(b.layer));
|
|
316
|
+
const ordered = [...parts].sort((a, b) => a.layer.localeCompare(b.layer) || a.sourceFile.localeCompare(b.sourceFile));
|
|
340
317
|
for (const part of ordered) {
|
|
341
|
-
const structureRules = (part.file.structure ?? []).map((entry) =>
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
318
|
+
const structureRules = (part.file.structure ?? []).map((entry) => {
|
|
319
|
+
const appliesTo = effectiveAppliesTo(part, entry.appliesTo);
|
|
320
|
+
return {
|
|
321
|
+
...entry,
|
|
322
|
+
id: effectiveRuleId(part, entry.id),
|
|
323
|
+
...(appliesTo ? { appliesTo } : { appliesTo: undefined }),
|
|
324
|
+
mode: normalizeMode(entry.mode),
|
|
325
|
+
provenance: provenanceFor(part, entry.id),
|
|
326
|
+
};
|
|
327
|
+
});
|
|
328
|
+
const invariantRules = (part.file.invariants ?? []).map((entry) => {
|
|
329
|
+
const appliesTo = effectiveAppliesTo(part, entry.appliesTo);
|
|
330
|
+
return {
|
|
331
|
+
...entry,
|
|
332
|
+
id: effectiveRuleId(part, entry.id),
|
|
333
|
+
...(appliesTo ? { appliesTo } : { appliesTo: undefined }),
|
|
334
|
+
mode: normalizeMode(entry.mode),
|
|
335
|
+
provenance: provenanceFor(part, entry.id),
|
|
336
|
+
};
|
|
337
|
+
});
|
|
338
|
+
const bucket = byLayer[part.layer];
|
|
339
|
+
if (!bucket) {
|
|
340
|
+
byLayer[part.layer] = {
|
|
354
341
|
sourceFile: part.sourceFile,
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
342
|
+
sourceFiles: [part.sourceFile],
|
|
343
|
+
structure: structureRules,
|
|
344
|
+
invariants: invariantRules,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
if (!bucket.sourceFiles)
|
|
349
|
+
bucket.sourceFiles = [bucket.sourceFile];
|
|
350
|
+
if (!bucket.sourceFiles.includes(part.sourceFile))
|
|
351
|
+
bucket.sourceFiles.push(part.sourceFile);
|
|
352
|
+
bucket.structure.push(...structureRules);
|
|
353
|
+
bucket.invariants.push(...invariantRules);
|
|
354
|
+
}
|
|
364
355
|
structure.push(...structureRules);
|
|
365
356
|
invariants.push(...invariantRules);
|
|
366
357
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { findClassDeclarations, scanClassMembers } from './class-source-scan.mjs';
|
|
12
|
-
import { sourceHasPersistenceWrite, sourceImportsPersistenceDriverText } from './persistence-write-hint.mjs';
|
|
12
|
+
import { sourceHasPersistenceWrite, sourceHasTaggedSqlWrite, sourceImportsPersistenceDriverText, } from './persistence-write-hint.mjs';
|
|
13
13
|
/** Keep in lockstep with arkRulesTypes.ARK_RULE_TIER2_SENSOR_IDS (self-contained for CLI gen). */
|
|
14
14
|
const ARK_RULE_TIER2_SENSOR_IDS = ['no-anemic-model'];
|
|
15
15
|
/**
|
|
@@ -35,7 +35,7 @@ const THIS_FIELD_ASSIGNMENT_RE = /\bthis\.[A-Za-z_][A-Za-z0-9_]*\s*=(?!=)/g;
|
|
|
35
35
|
export function expectedDomainInvariantWordsPhrase() {
|
|
36
36
|
return `${DOMAIN_INVARIANT_WORDS.join(', ')}, or events-array .push(`;
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
function referencesGuardOrPublish(source) {
|
|
39
39
|
return DOMAIN_INVARIANT_WORD_RE.test(source) || DOMAIN_EVENTS_PUSH_RE.test(source);
|
|
40
40
|
}
|
|
41
41
|
export function isIdiomaticEventsReset(source, assignIndex, methodName) {
|
|
@@ -468,10 +468,10 @@ export function collectEmptyInvariantCatalogFindings(input) {
|
|
|
468
468
|
},
|
|
469
469
|
];
|
|
470
470
|
}
|
|
471
|
-
|
|
471
|
+
function isPersistenceDriverLayer(layer) {
|
|
472
472
|
return layer === 'PersistenceAdapters';
|
|
473
473
|
}
|
|
474
|
-
|
|
474
|
+
function sourceImportsPersistenceDriver(content, resolvedImports) {
|
|
475
475
|
if (sourceImportsPersistenceDriverText(content))
|
|
476
476
|
return true;
|
|
477
477
|
if (!resolvedImports)
|
|
@@ -502,7 +502,9 @@ export function deriveArkRuleFileHints(_file, content, resolvedImports) {
|
|
|
502
502
|
if (!content)
|
|
503
503
|
return null;
|
|
504
504
|
const hasIo = sourceImportsPersistenceDriver(content, resolvedImports);
|
|
505
|
-
|
|
505
|
+
// A tagged-template SQL write head is evidence on its own (the use case composed
|
|
506
|
+
// the statement). ORM verbs and untagged SQL still need a driver import.
|
|
507
|
+
const persistenceWrite = (hasIo && sourceHasPersistenceWrite(content)) || sourceHasTaggedSqlWrite(content);
|
|
506
508
|
// Orchestration/adapter heuristics need a longer window; writes still fire on short probes.
|
|
507
509
|
if (content.length < 40) {
|
|
508
510
|
return persistenceWrite ? { persistenceWrite: true } : null;
|
package/bin/lib/check-args.mjs
CHANGED
|
@@ -24,7 +24,9 @@ export const LOCAL_STRICT_MERGE_MESSAGE =
|
|
|
24
24
|
'--local cannot be combined with --strict-merge: CI stays the full fail-closed check. Use --local (or ARK_CHECK_LOCAL=1) only on the local / pre-push path.';
|
|
25
25
|
|
|
26
26
|
export const LOCAL_TREE_MODE_MESSAGE =
|
|
27
|
-
'--local cannot be combined with report modes that need the whole tree (--doctor, --coverage, --plan, --report, --promote) or with --update-baseline. Use --local --base <ref> for the cheap local check.';
|
|
27
|
+
'--local cannot be combined with report modes that need the whole tree (--doctor, --coverage, --plan, --report, --promote, --probe-invariants) or with --update-baseline. Use --local --base <ref> for the cheap local check.';
|
|
28
|
+
|
|
29
|
+
const PROBE_RUNNERS = ['vitest', 'jest', 'node'];
|
|
28
30
|
|
|
29
31
|
export const UPDATE_BASELINE_SCOPE_MESSAGE =
|
|
30
32
|
'--update-baseline cannot be combined with a changed-files scope (--changed, --local, or a --persona that implies --changed): the baseline is a whole-tree record, and a changed-files scan would overwrite it with only that subset and delete every other frozen key. Drop the scope flag (ARK_CHECK_LOCAL=1 is ignored for a baseline update) and re-run.';
|
|
@@ -66,6 +68,7 @@ export function applyLocalCheckMode(args, env = process.env) {
|
|
|
66
68
|
args.plan && '--plan',
|
|
67
69
|
args.report && '--report',
|
|
68
70
|
args.promote && '--promote',
|
|
71
|
+
args.probeInvariants && '--probe-invariants',
|
|
69
72
|
args.updateBaseline && '--update-baseline',
|
|
70
73
|
].filter(Boolean);
|
|
71
74
|
if (args.strictMerge) {
|
|
@@ -201,6 +204,24 @@ export function parseArgs(argv, env = process.env) {
|
|
|
201
204
|
args.promote = next && !next.startsWith('-') ? argv[++i] : true;
|
|
202
205
|
}
|
|
203
206
|
}
|
|
207
|
+
else if (arg === '--probe-invariants' || arg.startsWith('--probe-invariants=')) {
|
|
208
|
+
// Same shape as --promote: bare probes every declared invariant; a value
|
|
209
|
+
// narrows to one id (`=<id>` also carries an id that begins with '-').
|
|
210
|
+
const eq = arg.indexOf('=');
|
|
211
|
+
if (eq > 0) {
|
|
212
|
+
args.probeInvariants = arg.slice(eq + 1) || true;
|
|
213
|
+
} else {
|
|
214
|
+
const next = argv[i + 1];
|
|
215
|
+
args.probeInvariants = next && !next.startsWith('-') ? argv[++i] : true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if (arg === '--runner') {
|
|
219
|
+
const value = requireValue(arg, i++).trim().toLowerCase();
|
|
220
|
+
if (!PROBE_RUNNERS.includes(value)) {
|
|
221
|
+
throw new Error(`--runner must be one of ${PROBE_RUNNERS.join(', ')} (got ${JSON.stringify(value)}).`);
|
|
222
|
+
}
|
|
223
|
+
args.runner = value;
|
|
224
|
+
}
|
|
204
225
|
else if (arg === '--apply') args.apply = true;
|
|
205
226
|
else if (arg === '--recommend') args.recommend = true;
|
|
206
227
|
else if (arg === '--write-plan') args.writePlan = true;
|
|
@@ -307,8 +328,49 @@ export function parseArgs(argv, env = process.env) {
|
|
|
307
328
|
);
|
|
308
329
|
}
|
|
309
330
|
}
|
|
331
|
+
assertProbeInvariantsAlone(args);
|
|
310
332
|
if (args.updateBaseline && (args.changed || args.local)) {
|
|
311
333
|
throw new Error(UPDATE_BASELINE_SCOPE_MESSAGE);
|
|
312
334
|
}
|
|
313
335
|
return applyLocalCheckMode(args, env);
|
|
314
336
|
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* ADR 0039 D1: the probe runs the project's own tests in a temporary copy, so
|
|
340
|
+
* it is never part of a check, the merge gate, status, or a changed-files scope.
|
|
341
|
+
* Every pairing that would put it on one of those paths is refused, and a
|
|
342
|
+
* report mode that answers first would make it silently do nothing.
|
|
343
|
+
*/
|
|
344
|
+
function assertProbeInvariantsAlone(args) {
|
|
345
|
+
if (args.runner && !args.probeInvariants) {
|
|
346
|
+
throw new Error('--runner applies to --probe-invariants. Run `arkgate-check --probe-invariants --runner <id>`.');
|
|
347
|
+
}
|
|
348
|
+
if (!args.probeInvariants) return;
|
|
349
|
+
const gatePaths = [
|
|
350
|
+
args.local && '--local',
|
|
351
|
+
args.changed && '--changed',
|
|
352
|
+
args.strictMerge && '--strict-merge',
|
|
353
|
+
args.updateBaseline && '--update-baseline',
|
|
354
|
+
args.doctor && '--doctor',
|
|
355
|
+
].filter(Boolean);
|
|
356
|
+
if (gatePaths.length > 0) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
`--probe-invariants cannot be combined with ${gatePaths.join(', ')}: the probe runs your tests in a temporary copy and is never part of a check, the merge gate, or status. Run it on its own.`
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
const shadowing = [
|
|
362
|
+
args.promote && '--promote',
|
|
363
|
+
args.sensors && '--sensors',
|
|
364
|
+
args.pathDrift && '--path-drift',
|
|
365
|
+
args.coverage && '--coverage',
|
|
366
|
+
args.plan && '--plan',
|
|
367
|
+
args.report && '--report',
|
|
368
|
+
args.rulesInventory && '--rules-inventory',
|
|
369
|
+
args.recommend && '--recommend',
|
|
370
|
+
].filter(Boolean);
|
|
371
|
+
if (shadowing.length > 0) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
`--probe-invariants cannot be combined with ${shadowing.join(', ')}: one of them would answer first and the other would silently do nothing. Run them separately.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
@@ -16,24 +16,13 @@ import {
|
|
|
16
16
|
buildAgentProjectionBlock,
|
|
17
17
|
} from './agent-projection.mjs';
|
|
18
18
|
import { getDiagnosticCatalogEntry } from './diagnostic-catalog.mjs';
|
|
19
|
-
import {
|
|
19
|
+
import { addPackageJsonPropertyPreservingFormat } from './field-install.mjs';
|
|
20
20
|
import { renderHostSupportMatrixMarkdown } from './host-support-matrix.mjs';
|
|
21
21
|
import { PREFERRED_MCP_BIN } from './hook-templates.mjs';
|
|
22
22
|
import { hasCheckArchitectureScript, readPackageJson } from './gate-files.mjs';
|
|
23
23
|
import { arkPackageVersion } from './skill-install.mjs';
|
|
24
24
|
import { arkgateIsProjectDependency, pinnedArkgateRunner } from './package-manager.mjs';
|
|
25
25
|
|
|
26
|
-
// Field-install helpers re-exported for callers that import from this module.
|
|
27
|
-
export {
|
|
28
|
-
ensureBaselineFlagInCheckCommand,
|
|
29
|
-
syncBaselineIntoCheckSurfaces,
|
|
30
|
-
pinArkgateDevDependency,
|
|
31
|
-
IO_DIR_SEGMENTS,
|
|
32
|
-
detectContractFalseGreenRisk,
|
|
33
|
-
FALSE_GREEN_GAP_ID,
|
|
34
|
-
falseGreenAdoptionGap,
|
|
35
|
-
} from './field-install.mjs';
|
|
36
|
-
|
|
37
26
|
export function checkArgsForRoot(root, { requireGates = false } = {}) {
|
|
38
27
|
const baselineFlag = fs.existsSync(path.join(root, '.ark-baseline.json'))
|
|
39
28
|
? ' --baseline .ark-baseline.json'
|
|
@@ -42,7 +31,6 @@ export function checkArgsForRoot(root, { requireGates = false } = {}) {
|
|
|
42
31
|
return `--root . --config ark.config.json ${profile}${baselineFlag}`;
|
|
43
32
|
}
|
|
44
33
|
|
|
45
|
-
|
|
46
34
|
export function packageManager(root) {
|
|
47
35
|
const pm = localPackageManager(root);
|
|
48
36
|
if (arkgateIsProjectDependency(root)) return pm;
|
|
@@ -114,65 +102,13 @@ export function checkArchitectureScriptSnippet(root) {
|
|
|
114
102
|
* @param {string} scriptValue
|
|
115
103
|
*/
|
|
116
104
|
function addPackageScriptPreservingFormat(source, scriptName, scriptValue) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (scriptsMatch) {
|
|
126
|
-
const open = source.indexOf('{', scriptsMatch.index);
|
|
127
|
-
let depth = 0;
|
|
128
|
-
let quoted = false;
|
|
129
|
-
let escaped = false;
|
|
130
|
-
let close = -1;
|
|
131
|
-
for (let index = open; index < source.length; index += 1) {
|
|
132
|
-
const char = source[index];
|
|
133
|
-
if (quoted) {
|
|
134
|
-
if (escaped) escaped = false;
|
|
135
|
-
else if (char === '\\') escaped = true;
|
|
136
|
-
else if (char === '"') quoted = false;
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
if (char === '"') quoted = true;
|
|
140
|
-
else if (char === '{') depth += 1;
|
|
141
|
-
else if (char === '}' && --depth === 0) {
|
|
142
|
-
close = index;
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
if (close === -1) throw new Error('Unbalanced package.json scripts object');
|
|
147
|
-
const body = source.slice(open + 1, close);
|
|
148
|
-
if (!multiline) {
|
|
149
|
-
const addition = body.trim() ? `,${key}:${encoded}` : `${key}:${encoded}`;
|
|
150
|
-
return `${source.slice(0, close)}${addition}${source.slice(close)}`;
|
|
151
|
-
}
|
|
152
|
-
const beforeClose = source.slice(0, close);
|
|
153
|
-
const trailing = beforeClose.match(/\s*$/)?.[0] ?? '';
|
|
154
|
-
const contentEnd = close - trailing.length;
|
|
155
|
-
const closingIndent = trailing.slice(trailing.lastIndexOf('\n') + 1);
|
|
156
|
-
const propertyIndent = `${closingIndent}${indentUnit}`;
|
|
157
|
-
const addition = body.trim()
|
|
158
|
-
? `,${eol}${propertyIndent}${key}: ${encoded}`
|
|
159
|
-
: `${propertyIndent}${key}: ${encoded}`;
|
|
160
|
-
return `${source.slice(0, contentEnd)}${addition}${eol}${closingIndent}${source.slice(close)}`;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const rootClose = source.lastIndexOf('}');
|
|
164
|
-
if (rootClose === -1) throw new Error('Unbalanced package.json object');
|
|
165
|
-
const rootBody = source.slice(0, rootClose);
|
|
166
|
-
if (!multiline) {
|
|
167
|
-
const separator = rootBody.trim().endsWith('{') ? '' : ',';
|
|
168
|
-
return `${rootBody}${separator}"scripts":{${key}:${encoded}}${source.slice(rootClose)}`;
|
|
169
|
-
}
|
|
170
|
-
const trailing = rootBody.match(/\s*$/)?.[0] ?? '';
|
|
171
|
-
const contentEnd = rootClose - trailing.length;
|
|
172
|
-
const rootClosingIndent = trailing.slice(trailing.lastIndexOf('\n') + 1);
|
|
173
|
-
const separator = source.slice(0, contentEnd).trimEnd().endsWith('{') ? '' : ',';
|
|
174
|
-
const addition = `${separator}${eol}${rootPropertyIndent}"scripts": {${eol}${rootPropertyIndent}${indentUnit}${key}: ${encoded}${eol}${rootPropertyIndent}}`;
|
|
175
|
-
return `${source.slice(0, contentEnd)}${addition}${eol}${rootClosingIndent}${source.slice(rootClose)}`;
|
|
105
|
+
return addPackageJsonPropertyPreservingFormat(
|
|
106
|
+
source,
|
|
107
|
+
'scripts',
|
|
108
|
+
scriptName,
|
|
109
|
+
scriptValue,
|
|
110
|
+
'package.json scripts'
|
|
111
|
+
);
|
|
176
112
|
}
|
|
177
113
|
|
|
178
114
|
/**
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Pure CLI helper (bin/lib/class-source-scan.mjs). Zero Node I/O.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const MEMBER_MODIFIERS = new Set([
|
|
12
12
|
'public',
|
|
13
13
|
'private',
|
|
14
14
|
'protected',
|
|
@@ -92,7 +92,7 @@ function skipRegexLiteral(src, index) {
|
|
|
92
92
|
* Index after a string / comment / regex literal starting at `index`, or `index`
|
|
93
93
|
* when none starts there.
|
|
94
94
|
*/
|
|
95
|
-
|
|
95
|
+
function skipStringOrComment(src, index) {
|
|
96
96
|
const ch = src[index];
|
|
97
97
|
if (ch === '/' && src[index + 1] === '/') {
|
|
98
98
|
const nl = src.indexOf('\n', index);
|
|
@@ -119,7 +119,7 @@ export function skipStringOrComment(src, index) {
|
|
|
119
119
|
}
|
|
120
120
|
return index;
|
|
121
121
|
}
|
|
122
|
-
|
|
122
|
+
function skipWsAndComments(src, index) {
|
|
123
123
|
let i = index;
|
|
124
124
|
while (i < src.length) {
|
|
125
125
|
if (/\s/.test(src[i])) {
|
|
@@ -134,7 +134,7 @@ export function skipWsAndComments(src, index) {
|
|
|
134
134
|
}
|
|
135
135
|
return i;
|
|
136
136
|
}
|
|
137
|
-
|
|
137
|
+
function readIdent(src, index) {
|
|
138
138
|
const ch = src[index];
|
|
139
139
|
if (!ch || !/[A-Za-z_$]/.test(ch))
|
|
140
140
|
return null;
|
|
@@ -148,7 +148,7 @@ export function readIdent(src, index) {
|
|
|
148
148
|
* never closes. Strings and comments are skipped. For `<`/`>`, an arrow `=>` is not
|
|
149
149
|
* a closing angle.
|
|
150
150
|
*/
|
|
151
|
-
|
|
151
|
+
function skipBalanced(src, openIndex, openCh, closeCh) {
|
|
152
152
|
if (src[openIndex] !== openCh)
|
|
153
153
|
return null;
|
|
154
154
|
let depth = 1;
|