arkgate 4.8.23 → 4.8.25
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 +153 -0
- package/README.md +7 -5
- package/bin/ark-check-runtime.mjs +41 -91
- package/bin/ark-dashboard.mjs +56 -53
- package/bin/ark-layer-match.mjs +707 -121
- package/bin/ark-mcp-runtime.mjs +448 -169
- package/bin/ark-mcp.mjs +68 -4
- package/bin/ark-shared.mjs +42 -22
- package/bin/ark.mjs +65 -97
- package/bin/lib/adapter-contract-types.mjs +5 -0
- package/bin/lib/adapter-contract.mjs +15 -0
- 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 +9 -5
- package/bin/lib/ambient-state.mjs +9 -30
- package/bin/lib/analysis-engine.mjs +15 -11
- package/bin/lib/architecture-scan.mjs +61 -15
- package/bin/lib/ark-order-doctor.mjs +4 -29
- package/bin/lib/ark-order-facts.mjs +319 -17
- package/bin/lib/ark-order-sensors.mjs +28 -101
- package/bin/lib/ark-order-types.mjs +3 -2
- package/bin/lib/ark-run-doctor.mjs +17 -28
- package/bin/lib/ark-run-facts.mjs +461 -48
- package/bin/lib/ark-run-sensors.mjs +6 -4
- package/bin/lib/arkgate-pins.mjs +91 -0
- package/bin/lib/arkrule-file-hints.mjs +2 -47
- package/bin/lib/arkrules-contract.mjs +124 -133
- package/bin/lib/arkrules-sensors.mjs +55 -252
- package/bin/lib/check-args.mjs +85 -1
- package/bin/lib/ci-and-commands.mjs +24 -78
- package/bin/lib/ci-merge-boundary.mjs +1 -1
- package/bin/lib/class-source-scan.mjs +479 -0
- package/bin/lib/clone-detection.mjs +474 -0
- package/bin/lib/codex-home.mjs +5 -3
- package/bin/lib/config-contract-slices.mjs +739 -0
- package/bin/lib/config-contract.mjs +42 -607
- package/bin/lib/config-extras.mjs +16 -22
- package/bin/lib/config-warnings.mjs +32 -0
- package/bin/lib/contract-smells.mjs +33 -15
- package/bin/lib/core-ratchet.mjs +0 -2
- package/bin/lib/dashboard-cli.mjs +62 -0
- package/bin/lib/deep-module-coach.mjs +2 -4
- package/bin/lib/deploy-path.mjs +13 -38
- package/bin/lib/design-delta.mjs +1 -1
- package/bin/lib/design-smells.mjs +2 -2
- package/bin/lib/diagnostic-catalog.mjs +23 -9
- package/bin/lib/doctor-advisories.mjs +78 -8
- package/bin/lib/doctor-green-cite.mjs +1 -1
- package/bin/lib/doctor-human.mjs +1 -1
- package/bin/lib/doctor-next-actions.mjs +19 -4
- package/bin/lib/doctor-plan.mjs +9 -13
- package/bin/lib/duplication-io.mjs +572 -0
- package/bin/lib/effective-contract-load.mjs +282 -83
- package/bin/lib/enforcement-profiles.mjs +40 -4
- 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 +10 -5
- package/bin/lib/flat-parent-importers.mjs +139 -0
- package/bin/lib/flat-parent-pilot.mjs +40 -100
- package/bin/lib/gate-files.mjs +58 -90
- package/bin/lib/git-change-scope.mjs +109 -0
- package/bin/lib/github-enforcement.mjs +166 -29
- package/bin/lib/golden-pattern.mjs +1 -1
- package/bin/lib/hook-templates.mjs +17 -15
- package/bin/lib/host-support-matrix.mjs +3 -1
- 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/import-resolve.mjs +13 -1
- package/bin/lib/improvement-compass-doctor.mjs +1 -24
- package/bin/lib/install-migrate.mjs +50 -73
- package/bin/lib/invariant-coverage-io.mjs +4 -3
- package/bin/lib/invariant-coverage.mjs +85 -11
- 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 +13 -6
- package/bin/lib/mcp-hook-payload.mjs +403 -63
- 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 +60 -2
- package/bin/lib/parse-health.mjs +1 -1
- package/bin/lib/passthrough-commands.mjs +83 -0
- package/bin/lib/persistence-write-hint.mjs +106 -0
- package/bin/lib/pin-dependent-gates.mjs +117 -0
- package/bin/lib/policy-delta-io.mjs +219 -14
- package/bin/lib/post-green-path.mjs +1 -1
- package/bin/lib/prepare-change.mjs +115 -7
- package/bin/lib/prepare-write.mjs +2 -2
- package/bin/lib/presets.mjs +59 -73
- package/bin/lib/product-copy.mjs +1 -6
- package/bin/lib/project-root.mjs +31 -62
- package/bin/lib/remediation.mjs +23 -4
- package/bin/lib/resolved-candidate-facts.mjs +405 -117
- package/bin/lib/rules-inventory.mjs +22 -3
- package/bin/lib/rules-under-contract.mjs +247 -9
- package/bin/lib/scan-files.mjs +36 -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 +140 -6
- package/bin/lib/source-policy.mjs +34 -0
- package/bin/lib/start-install-recovery.mjs +72 -2
- package/bin/lib/start-preview.mjs +31 -1
- package/bin/lib/states-transitions-presence.mjs +3 -3
- package/bin/lib/status-command.mjs +35 -22
- package/bin/lib/status-manifest.mjs +50 -30
- package/bin/lib/status-transition-catalog.mjs +0 -6
- package/bin/lib/suggestions.mjs +3 -3
- package/bin/lib/team-parliament-io.mjs +76 -88
- package/bin/lib/team-parliament.mjs +21 -10
- 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 +6 -4
- package/dist/chunk-A5EIY3HI.cjs +5 -0
- package/dist/chunk-AHACEKSV.js +16 -0
- package/dist/chunk-SVSQVGFJ.js +5 -0
- package/dist/chunk-UN5EH4QV.cjs +16 -0
- package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-GPvvdleM.d.cts} +30 -4
- package/dist/configTypes-GPvvdleM.d.ts +257 -0
- package/dist/eslint/index.cjs +18 -5
- package/dist/eslint/index.d.cts +368 -0
- package/dist/eslint/index.d.ts +139 -156
- package/dist/eslint/index.js +11 -5
- package/dist/index.cjs +36 -35
- package/dist/index.d.cts +5547 -0
- package/dist/index.d.ts +3283 -198
- package/dist/index.js +36 -35
- package/dist/inspectorListen-6DQVJ4DC.js +1 -0
- package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
- package/dist/nestjs/index.cjs +1 -18
- package/dist/nestjs/index.d.cts +22 -0
- package/dist/nestjs/index.d.ts +1 -3
- package/dist/nestjs/index.js +1 -18
- package/dist/order/index.cjs +1 -1
- package/dist/order/index.d.cts +155 -0
- package/dist/order/index.d.ts +15 -3
- package/dist/order/index.js +1 -1
- package/dist/runtime/index.cjs +13 -30
- package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +828 -276
- package/dist/runtime/index.d.ts +2741 -133
- package/dist/runtime/index.js +13 -30
- package/dist/{types-Bdj32QwA.d.ts → types-CnauMQQL.d.cts} +607 -163
- package/dist/types-CnauMQQL.d.ts +1800 -0
- package/docs/README.md +3 -1
- package/docs/agent-guide.md +217 -16
- package/docs/ai-gates.md +174 -26
- package/docs/arkorder.md +32 -15
- package/docs/brownfield-adoption.md +5 -1
- package/docs/configuration.md +230 -31
- package/docs/develop.md +2 -1
- package/docs/diagnostics.md +212 -19
- package/docs/package-surface.md +35 -9
- package/docs/product-voice.md +7 -1
- package/package.json +69 -19
- package/schemas/ark.analysis-result.schema.json +15 -0
- package/schemas/ark.config.schema.json +77 -12
- package/schemas/ark.invariant-probe.schema.json +368 -0
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
- package/schemas/ark.status-manifest.schema.json +22 -0
- package/server.json +3 -9
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +5 -1
- 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-order/SKILL.md +4 -2
- package/templates/agent-skills/ark-place/SKILL.md +8 -0
- package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
- package/templates/arkrules/PersistenceAdapters.json +1 -1
- package/templates/arkrules/PresentationAdapters.json +1 -1
- package/templates/skills/ark-adopt.md +5 -1
- 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-order.md +4 -2
- package/templates/skills/ark-place.md +8 -0
- package/templates/skills/ark-runtime.md +6 -1
- package/bin/lib/ark-order-error.mjs +0 -18
- package/bin/lib/ark-order-invariants.mjs +0 -346
- package/dist/types-Ik_OakcI.d.ts +0 -183
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
unavailableStatusImprovementCompass,
|
|
18
18
|
} from './status-manifest.mjs';
|
|
19
19
|
import { createProjectId } from './project-identity.mjs';
|
|
20
|
+
import { parseArkConfigJson } from './config-contract.mjs';
|
|
20
21
|
import { resolveEffectiveProjectRoot } from './project-root.mjs';
|
|
21
22
|
import { detectWritePathCapabilities } from './write-path-detect.mjs';
|
|
22
23
|
import { buildWritePathHonesty } from './enforcement-honesty.mjs';
|
|
@@ -29,6 +30,7 @@ import { projectStatusArkRun } from './ark-run-doctor.mjs';
|
|
|
29
30
|
import { projectStatusArkOrder } from './ark-order-doctor.mjs';
|
|
30
31
|
import { collectVsBaseFacts, discoverTeamBaseRef } from './team-parliament-io.mjs';
|
|
31
32
|
import { classifyAdopted, readAdoptionStance } from './adoption-stance.mjs';
|
|
33
|
+
import { lastCheckVerdictFacts } from './upstream-report.mjs';
|
|
32
34
|
|
|
33
35
|
function sha256Hex(value) {
|
|
34
36
|
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
@@ -114,24 +116,7 @@ export function lastCheckFactsFromSnapshot(latest, baseline) {
|
|
|
114
116
|
frozenResidual,
|
|
115
117
|
};
|
|
116
118
|
}
|
|
117
|
-
const at =
|
|
118
|
-
typeof latest.generatedAt === 'string'
|
|
119
|
-
? latest.generatedAt
|
|
120
|
-
: typeof latest.at === 'string'
|
|
121
|
-
? latest.at
|
|
122
|
-
: null;
|
|
123
|
-
const active =
|
|
124
|
-
typeof latest.activeViolations === 'number'
|
|
125
|
-
? latest.activeViolations
|
|
126
|
-
: typeof latest.violations?.active === 'number'
|
|
127
|
-
? latest.violations.active
|
|
128
|
-
: null;
|
|
129
|
-
let verdict = null;
|
|
130
|
-
if (latest.ok === true && (active == null || active === 0)) verdict = 'pass';
|
|
131
|
-
else if (latest.ok === false || (typeof active === 'number' && active > 0)) verdict = 'fail';
|
|
132
|
-
else if (latest.completeness === 'partial' || latest.completeness === 'unavailable') {
|
|
133
|
-
verdict = 'incomplete';
|
|
134
|
-
} else if (latest.ok === true) verdict = 'pass';
|
|
119
|
+
const { at, active, verdict } = lastCheckVerdictFacts(latest);
|
|
135
120
|
return {
|
|
136
121
|
lastCheckAt: at,
|
|
137
122
|
lastCheckVerdict: verdict,
|
|
@@ -158,7 +143,7 @@ export function statusCompassFromSnapshot(latest, opts = {}) {
|
|
|
158
143
|
return unavailableStatusImprovementCompass({
|
|
159
144
|
reasonCode: 'NO_SESSION_SNAPSHOT',
|
|
160
145
|
reason:
|
|
161
|
-
'No session report snapshot yet — run ark-check --
|
|
146
|
+
'No session report snapshot yet — run ark-check --report to write the session snapshot (--doctor shows full lenses but does not write it). Status never invents green.',
|
|
162
147
|
contractHash,
|
|
163
148
|
});
|
|
164
149
|
}
|
|
@@ -189,7 +174,7 @@ export function statusCompassFromSnapshot(latest, opts = {}) {
|
|
|
189
174
|
reasonCode: complete ? undefined : 'FACTS_PARTIAL',
|
|
190
175
|
reason: complete
|
|
191
176
|
? undefined
|
|
192
|
-
: 'Session snapshot residual is partial — re-run
|
|
177
|
+
: 'Session snapshot residual is partial — re-run ark-check --report for full compass.',
|
|
193
178
|
factsSource: 'report-snapshot',
|
|
194
179
|
contractHash,
|
|
195
180
|
});
|
|
@@ -198,7 +183,7 @@ export function statusCompassFromSnapshot(latest, opts = {}) {
|
|
|
198
183
|
return unavailableStatusImprovementCompass({
|
|
199
184
|
reasonCode: 'NO_SESSION_SNAPSHOT',
|
|
200
185
|
reason:
|
|
201
|
-
'Session snapshot has no improvement compass facts — run ark-check --
|
|
186
|
+
'Session snapshot has no improvement compass facts — run ark-check --report to refresh the session snapshot. Status never invents green.',
|
|
202
187
|
contractHash,
|
|
203
188
|
});
|
|
204
189
|
}
|
|
@@ -242,6 +227,24 @@ export function thinStatusCompassFromDoctor(doctorCompass, opts = {}) {
|
|
|
242
227
|
* contractHash?: string | null,
|
|
243
228
|
* }} [options]
|
|
244
229
|
*/
|
|
230
|
+
/**
|
|
231
|
+
* Run the shared contract validator (same rules as ark-check / MCP / ESLint).
|
|
232
|
+
* @param {string} text
|
|
233
|
+
* @param {string} source
|
|
234
|
+
* @returns {string[]} `<path>: <message>` lines; empty when valid
|
|
235
|
+
*/
|
|
236
|
+
function validateConfigText(text, source) {
|
|
237
|
+
try {
|
|
238
|
+
parseArkConfigJson(text, source);
|
|
239
|
+
return [];
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (Array.isArray(error?.issues) && error.issues.length > 0) {
|
|
242
|
+
return error.issues.map((issue) => `${issue.path}: ${issue.message}`);
|
|
243
|
+
}
|
|
244
|
+
return [error instanceof Error ? error.message : String(error)];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
245
248
|
export function collectStatusFacts(options = {}) {
|
|
246
249
|
const startRoot = path.resolve(options.root || process.cwd());
|
|
247
250
|
const configName = options.config || 'ark.config.json';
|
|
@@ -256,12 +259,17 @@ export function collectStatusFacts(options = {}) {
|
|
|
256
259
|
const configExists = fs.existsSync(configPath);
|
|
257
260
|
|
|
258
261
|
let config = null;
|
|
262
|
+
/** Shared contract validator messages — status must not look authoritative-green on a config every gate refuses. */
|
|
263
|
+
let configErrors = [];
|
|
259
264
|
if (configExists) {
|
|
265
|
+
let text = '';
|
|
260
266
|
try {
|
|
261
|
-
|
|
267
|
+
text = fs.readFileSync(configPath, 'utf8');
|
|
268
|
+
config = JSON.parse(text);
|
|
262
269
|
} catch {
|
|
263
270
|
config = null;
|
|
264
271
|
}
|
|
272
|
+
configErrors = validateConfigText(text, path.basename(configPath));
|
|
265
273
|
}
|
|
266
274
|
|
|
267
275
|
const projectId = configExists
|
|
@@ -390,6 +398,7 @@ export function collectStatusFacts(options = {}) {
|
|
|
390
398
|
arkgateVersion: options.arkgateVersion || packageVersion(),
|
|
391
399
|
resolvedRoot: realpathOrResolve(resolvedRoot),
|
|
392
400
|
resolvedConfigPath: configExists ? realpathOrResolve(configPath) : null,
|
|
401
|
+
configErrors,
|
|
393
402
|
projectId,
|
|
394
403
|
expectation,
|
|
395
404
|
expectedRootRelation,
|
|
@@ -564,6 +573,10 @@ export function runStatusCommand(args = {}) {
|
|
|
564
573
|
' · not a score'
|
|
565
574
|
);
|
|
566
575
|
}
|
|
576
|
+
if (manifest.contract?.valid === false) {
|
|
577
|
+
write(` contract: invalid (${manifest.contract.errors.length} issue(s)) — every enforcing surface refuses this config`);
|
|
578
|
+
for (const issue of manifest.contract.errors.slice(0, 5)) write(` - ${issue}`);
|
|
579
|
+
}
|
|
567
580
|
write(` next: [${manifest.nextAction.id}] ${manifest.nextAction.summary}`);
|
|
568
581
|
}
|
|
569
582
|
|
|
@@ -10,6 +10,22 @@
|
|
|
10
10
|
|
|
11
11
|
import { projectStatusArkRun } from './ark-run-doctor.mjs';
|
|
12
12
|
import { projectStatusArkOrder } from './ark-order-doctor.mjs';
|
|
13
|
+
/** Status slice schema of one extra plane (ArkRun / ArkOrder). */
|
|
14
|
+
function extraPlaneStatusSchema(plane) {
|
|
15
|
+
return {
|
|
16
|
+
type: 'object',
|
|
17
|
+
description: `${plane} extra residual (notAScore). present/mode from config; residual is a finding-id count (null = unknown, not green). extraMergeTeeth is honesty, never a score.`,
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
required: ['notAScore', 'present', 'mode', 'extraMergeTeeth', 'residual'],
|
|
20
|
+
properties: {
|
|
21
|
+
notAScore: { const: true },
|
|
22
|
+
present: { type: 'boolean' },
|
|
23
|
+
mode: { anyOf: [{ enum: ['advisory', 'enforced'] }, { type: 'null' }] },
|
|
24
|
+
extraMergeTeeth: { type: 'boolean' },
|
|
25
|
+
residual: { anyOf: [{ type: 'integer', minimum: 0 }, { type: 'null' }] },
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
13
29
|
export const ARK_STATUS_MANIFEST_SCHEMA_VERSION = '1.0';
|
|
14
30
|
export const ARK_STATUS_MANIFEST_SCHEMA_URL = 'https://unpkg.com/arkgate@4/schemas/ark.status-manifest.schema.json';
|
|
15
31
|
/**
|
|
@@ -175,6 +191,11 @@ export function defaultHonestLabel(writePath, host) {
|
|
|
175
191
|
* Deterministic next action from residual facts (no LLM).
|
|
176
192
|
* Prefer explicit override from productHonesty when provided.
|
|
177
193
|
*/
|
|
194
|
+
function normalizedConfigErrors(errors) {
|
|
195
|
+
if (!Array.isArray(errors))
|
|
196
|
+
return [];
|
|
197
|
+
return errors.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
198
|
+
}
|
|
178
199
|
export function resolveStatusNextAction(facts, binding, activation, lastCheck, rules) {
|
|
179
200
|
if (facts.nextActionOverride?.id && facts.nextActionOverride.summary) {
|
|
180
201
|
return {
|
|
@@ -202,22 +223,31 @@ export function resolveStatusNextAction(facts, binding, activation, lastCheck, r
|
|
|
202
223
|
summary: 'No ark.config.json found — run ark start (preview) then ark start --apply.',
|
|
203
224
|
};
|
|
204
225
|
}
|
|
226
|
+
const configErrors = normalizedConfigErrors(facts.configErrors);
|
|
227
|
+
if (configErrors.length > 0) {
|
|
228
|
+
return {
|
|
229
|
+
id: 'fix-config',
|
|
230
|
+
summary: `The Ark config is invalid, so ark-check, MCP and ESLint refuse it — fix: ${configErrors
|
|
231
|
+
.slice(0, 3)
|
|
232
|
+
.join('; ')}${configErrors.length > 3 ? ` (+${configErrors.length - 3} more)` : ''}`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
205
235
|
if (lastCheck.verdict === 'fail' || (lastCheck.activeViolations ?? 0) > 0) {
|
|
206
236
|
return {
|
|
207
237
|
id: 'fix-active-violations',
|
|
208
|
-
summary: `Clear ${lastCheck.activeViolations ?? 'active'} blocking architecture finding(s), then re-run ark-check
|
|
238
|
+
summary: `Clear ${lastCheck.activeViolations ?? 'active'} blocking architecture finding(s), then re-run ark-check --report to refresh the last-check snapshot (a plain check or --doctor does not update .ark/reports/latest.json).`,
|
|
209
239
|
};
|
|
210
240
|
}
|
|
211
241
|
if (lastCheck.verdict === 'incomplete') {
|
|
212
242
|
return {
|
|
213
243
|
id: 'restore-complete-analysis',
|
|
214
|
-
summary: 'Last check was incomplete — restore TypeScript/analysis inputs and re-run ark-check.',
|
|
244
|
+
summary: 'Last check was incomplete — restore TypeScript/analysis inputs and re-run ark-check --report to refresh the snapshot.',
|
|
215
245
|
};
|
|
216
246
|
}
|
|
217
247
|
if (lastCheck.verdict == null && lastCheck.at == null) {
|
|
218
248
|
return {
|
|
219
249
|
id: 'run-ark-check',
|
|
220
|
-
summary: 'No last-check snapshot yet — run ark-check --report (
|
|
250
|
+
summary: 'No last-check snapshot yet — run ark-check --report to freeze session evidence (--doctor does not write the snapshot).',
|
|
221
251
|
};
|
|
222
252
|
}
|
|
223
253
|
if (activation.writePath === 'unavailable') {
|
|
@@ -334,6 +364,10 @@ export function buildStatusManifest(facts) {
|
|
|
334
364
|
rules,
|
|
335
365
|
nextAction: resolveStatusNextAction(facts, binding, activation, lastCheck, rules),
|
|
336
366
|
};
|
|
367
|
+
const configErrors = normalizedConfigErrors(facts.configErrors);
|
|
368
|
+
if (projectIdentity.resolvedConfigPath && configErrors.length > 0) {
|
|
369
|
+
status.contract = { valid: false, errors: configErrors };
|
|
370
|
+
}
|
|
337
371
|
const compass = normalizeStatusImprovementCompass(facts.improvementCompass);
|
|
338
372
|
if (compass)
|
|
339
373
|
status.improvementCompass = compass;
|
|
@@ -433,7 +467,7 @@ export function unavailableStatusImprovementCompass(input = {}) {
|
|
|
433
467
|
topResidual: [],
|
|
434
468
|
reasonCode: input.reasonCode ?? STATUS_COMPASS_REASON_CODES.NO_SESSION_SNAPSHOT,
|
|
435
469
|
reason: input.reason ??
|
|
436
|
-
'No session compass facts yet — run ark-check --
|
|
470
|
+
'No session compass facts yet — run ark-check --report to write the session snapshot (--doctor shows full lenses but does not write it). Status never invents green.',
|
|
437
471
|
factsSource: 'none',
|
|
438
472
|
contractHash: input.contractHash,
|
|
439
473
|
});
|
|
@@ -591,6 +625,16 @@ export const ARK_STATUS_MANIFEST_SCHEMA = {
|
|
|
591
625
|
summary: { type: 'string', minLength: 1 },
|
|
592
626
|
},
|
|
593
627
|
},
|
|
628
|
+
contract: {
|
|
629
|
+
type: 'object',
|
|
630
|
+
description: 'Present only when the resolved config fails the shared contract validator (the same fail-closed rules ark-check, MCP and ESLint apply).',
|
|
631
|
+
additionalProperties: false,
|
|
632
|
+
required: ['valid', 'errors'],
|
|
633
|
+
properties: {
|
|
634
|
+
valid: { const: false },
|
|
635
|
+
errors: { type: 'array', minItems: 1, items: { type: 'string', minLength: 1 } },
|
|
636
|
+
},
|
|
637
|
+
},
|
|
594
638
|
improvementCompass: {
|
|
595
639
|
type: 'object',
|
|
596
640
|
description: 'Thin improvement-compass residual ids with honesty mode (notAScore). full | subset | unavailable. Never a gate input; full lenses on doctor JSON. When full, residual ids ⊆ doctor residual for the same facts. unavailable never invents green residual.',
|
|
@@ -625,31 +669,7 @@ export const ARK_STATUS_MANIFEST_SCHEMA = {
|
|
|
625
669
|
baselineGrew: { type: 'boolean' },
|
|
626
670
|
},
|
|
627
671
|
},
|
|
628
|
-
arkRun:
|
|
629
|
-
|
|
630
|
-
description: 'ArkRun extra residual (notAScore). present/mode from config; residual is a finding-id count (null = unknown, not green). extraMergeTeeth is honesty, never a score.',
|
|
631
|
-
additionalProperties: false,
|
|
632
|
-
required: ['notAScore', 'present', 'mode', 'extraMergeTeeth', 'residual'],
|
|
633
|
-
properties: {
|
|
634
|
-
notAScore: { const: true },
|
|
635
|
-
present: { type: 'boolean' },
|
|
636
|
-
mode: { anyOf: [{ enum: ['advisory', 'enforced'] }, { type: 'null' }] },
|
|
637
|
-
extraMergeTeeth: { type: 'boolean' },
|
|
638
|
-
residual: { anyOf: [{ type: 'integer', minimum: 0 }, { type: 'null' }] },
|
|
639
|
-
},
|
|
640
|
-
},
|
|
641
|
-
arkOrder: {
|
|
642
|
-
type: 'object',
|
|
643
|
-
description: 'ArkOrder extra residual (notAScore). present/mode from config; residual is a finding-id count (null = unknown, not green). extraMergeTeeth is honesty, never a score.',
|
|
644
|
-
additionalProperties: false,
|
|
645
|
-
required: ['notAScore', 'present', 'mode', 'extraMergeTeeth', 'residual'],
|
|
646
|
-
properties: {
|
|
647
|
-
notAScore: { const: true },
|
|
648
|
-
present: { type: 'boolean' },
|
|
649
|
-
mode: { anyOf: [{ enum: ['advisory', 'enforced'] }, { type: 'null' }] },
|
|
650
|
-
extraMergeTeeth: { type: 'boolean' },
|
|
651
|
-
residual: { anyOf: [{ type: 'integer', minimum: 0 }, { type: 'null' }] },
|
|
652
|
-
},
|
|
653
|
-
},
|
|
672
|
+
arkRun: extraPlaneStatusSchema('ArkRun'),
|
|
673
|
+
arkOrder: extraPlaneStatusSchema('ArkOrder'),
|
|
654
674
|
},
|
|
655
675
|
};
|
|
@@ -17,12 +17,6 @@ import { layerForFile } from '../ark-shared.mjs';
|
|
|
17
17
|
import { isDomainRoleLayerName } from './arkrules-sensors.mjs';
|
|
18
18
|
import { collectStatesTransitionsResidual } from './states-transitions-presence.mjs';
|
|
19
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
20
|
const SOURCE_EXT = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs', '.cjs']);
|
|
27
21
|
const MAX_FILE_BYTES = 200_000;
|
|
28
22
|
const MAX_ENTITIES = 8;
|
package/bin/lib/suggestions.mjs
CHANGED
|
@@ -19,7 +19,7 @@ export function dirSegmentsFromGlob(pattern) {
|
|
|
19
19
|
* Basename / path tokens that score as Persistence (data clients, auth, CRM).
|
|
20
20
|
* Used so adopt never maps lib/turso → Presentation (NEW-ADOPT-LIB-AS-PRESENTATION).
|
|
21
21
|
*/
|
|
22
|
-
|
|
22
|
+
const PERSISTENCE_PATH_TOKENS = Object.freeze([
|
|
23
23
|
'turso',
|
|
24
24
|
'prisma',
|
|
25
25
|
'supabase',
|
|
@@ -64,7 +64,7 @@ export function isPersistenceClientPath(relPath) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/** True when a path looks like pure domain folders. */
|
|
67
|
-
|
|
67
|
+
function isDomainPath(relPath) {
|
|
68
68
|
const posix = String(relPath || '')
|
|
69
69
|
.split(/[/\\]/)
|
|
70
70
|
.filter(Boolean)
|
|
@@ -81,7 +81,7 @@ let _layerByDir;
|
|
|
81
81
|
// Map<dirBasename, string[] layers>. A basename mapping to >1 layer (e.g. `app` — Application
|
|
82
82
|
// orchestration in the 11-layer defaults, but Presentation in the monorepo/Next preset) is
|
|
83
83
|
// genuinely ambiguous; every candidate is surfaced rather than silently picked.
|
|
84
|
-
|
|
84
|
+
function layerByDir() {
|
|
85
85
|
if (_layerByDir) return _layerByDir;
|
|
86
86
|
const map = new Map();
|
|
87
87
|
const add = (segment, layer) => {
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
* Team parliament I/O — git base refs, changed paths, pin/contract/baseline compare.
|
|
3
3
|
* Pure classification lives in team-parliament.mjs (Domain).
|
|
4
4
|
*/
|
|
5
|
-
import { spawnSync } from 'node:child_process';
|
|
6
5
|
import fs from 'node:fs';
|
|
7
6
|
import path from 'node:path';
|
|
8
7
|
import { createHash } from 'node:crypto';
|
|
9
|
-
import { applyAdvisorySiblingRatchet } from '../ark-layer-match.mjs';
|
|
8
|
+
import { applyAdvisorySiblingRatchet, globToRegExp } from '../ark-layer-match.mjs';
|
|
10
9
|
import {
|
|
11
10
|
baselineKeysFromDocument,
|
|
12
11
|
classifyBaselineKeyDelta,
|
|
@@ -20,39 +19,17 @@ import {
|
|
|
20
19
|
resolveStewardHandle,
|
|
21
20
|
suggestStewards,
|
|
22
21
|
} from './team-parliament.mjs';
|
|
22
|
+
import {
|
|
23
|
+
TEAM_BASE_CANDIDATES,
|
|
24
|
+
discoverTeamBaseRef,
|
|
25
|
+
gitShowText,
|
|
26
|
+
listChangedPaths,
|
|
27
|
+
runGit,
|
|
28
|
+
safeGitRef,
|
|
29
|
+
} from './git-change-scope.mjs';
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
classifyBaselineKeyDelta,
|
|
27
|
-
classifyChangeSet,
|
|
28
|
-
evaluateTeamGate,
|
|
29
|
-
formatVsBaseLine,
|
|
30
|
-
isTeamPersona,
|
|
31
|
-
mapPolicyClassToKind,
|
|
32
|
-
parseCodeownersHandles,
|
|
33
|
-
personaCheckBudget,
|
|
34
|
-
resolveStewardHandle,
|
|
35
|
-
suggestStewards,
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
/** Kill hung git instead of stalling CI. */
|
|
39
|
-
export const SPAWN_TIMEOUT_MS = 8000;
|
|
40
|
-
|
|
41
|
-
function runGit(cwd, args) {
|
|
42
|
-
return spawnSync('git', ['-C', cwd, ...args], {
|
|
43
|
-
encoding: 'utf8',
|
|
44
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
45
|
-
timeout: SPAWN_TIMEOUT_MS,
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function safeGitRef(value) {
|
|
50
|
-
return (
|
|
51
|
-
typeof value === 'string' &&
|
|
52
|
-
/^[A-Za-z0-9][A-Za-z0-9._/-]{0,200}$/.test(value) &&
|
|
53
|
-
!value.includes('..')
|
|
54
|
-
);
|
|
55
|
-
}
|
|
31
|
+
// prettier-ignore
|
|
32
|
+
export { discoverTeamBaseRef, safeGitRef };
|
|
56
33
|
|
|
57
34
|
export function contractSessionFrom(args, env = process.env) {
|
|
58
35
|
if (args?.contractSession === true) return true;
|
|
@@ -70,49 +47,6 @@ export function resolveTeamAuthor(args, env = process.env) {
|
|
|
70
47
|
});
|
|
71
48
|
}
|
|
72
49
|
|
|
73
|
-
export function discoverTeamBaseRef(root, preferred) {
|
|
74
|
-
if (safeGitRef(preferred)) return preferred;
|
|
75
|
-
const candidates = ['origin/dev', 'origin/main', 'origin/master', 'dev', 'main'];
|
|
76
|
-
for (const candidate of candidates) {
|
|
77
|
-
const exists = runGit(root, ['rev-parse', '--verify', `${candidate}^{commit}`]);
|
|
78
|
-
if (exists.status === 0) return candidate;
|
|
79
|
-
}
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function listChangedPaths(root, baseRef) {
|
|
84
|
-
if (!safeGitRef(baseRef)) {
|
|
85
|
-
return { ok: false, paths: [], error: 'Invalid or missing git base ref.' };
|
|
86
|
-
}
|
|
87
|
-
const verify = runGit(root, ['rev-parse', '--verify', `${baseRef}^{commit}`]);
|
|
88
|
-
if (verify.status !== 0) {
|
|
89
|
-
return { ok: false, paths: [], error: `Cannot resolve git ref ${baseRef}.` };
|
|
90
|
-
}
|
|
91
|
-
const diff = runGit(root, ['diff', '--name-only', `${baseRef}...HEAD`]);
|
|
92
|
-
const unstaged = runGit(root, ['diff', '--name-only']);
|
|
93
|
-
const staged = runGit(root, ['diff', '--name-only', '--cached']);
|
|
94
|
-
const untracked = runGit(root, ['ls-files', '--others', '--exclude-standard']);
|
|
95
|
-
if (diff.status !== 0) {
|
|
96
|
-
return { ok: false, paths: [], error: diff.stderr.trim() || `git diff ${baseRef} failed.` };
|
|
97
|
-
}
|
|
98
|
-
const paths = [
|
|
99
|
-
...diff.stdout.split('\n'),
|
|
100
|
-
...unstaged.stdout.split('\n'),
|
|
101
|
-
...staged.stdout.split('\n'),
|
|
102
|
-
...untracked.stdout.split('\n'),
|
|
103
|
-
]
|
|
104
|
-
.map((line) => line.trim().replace(/\\/g, '/'))
|
|
105
|
-
.filter(Boolean);
|
|
106
|
-
return { ok: true, paths: [...new Set(paths)].sort(), error: null };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function gitShowText(root, baseRef, relPath) {
|
|
110
|
-
if (!safeGitRef(baseRef) || !relPath) return null;
|
|
111
|
-
const shown = runGit(root, ['show', `${baseRef}:${relPath}`]);
|
|
112
|
-
if (shown.status !== 0) return null;
|
|
113
|
-
return shown.stdout;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
50
|
export function readJsonMaybe(text) {
|
|
117
51
|
if (typeof text !== 'string' || !text.trim()) return null;
|
|
118
52
|
try {
|
|
@@ -193,8 +127,11 @@ export function bindTeamBaseRefs(args, root) {
|
|
|
193
127
|
if (!next.against && (next.changed || next.contractDiff || next.persona)) {
|
|
194
128
|
next.against = teamBase;
|
|
195
129
|
}
|
|
196
|
-
|
|
130
|
+
// A contract session must classify the law change, or loosening skips steward/ack checks.
|
|
131
|
+
// policyBaseFromTeam: a base without the contract yet is adoption (no predecessor), not an error.
|
|
132
|
+
if (!next.policyBaseRef && (next.contractDiff || next.changed || next.against || next.contractSession)) {
|
|
197
133
|
next.policyBaseRef = teamBase;
|
|
134
|
+
next.policyBaseFromTeam = true;
|
|
198
135
|
}
|
|
199
136
|
if (!next.baseRef && next.failOnNewSmells) next.baseRef = teamBase;
|
|
200
137
|
}
|
|
@@ -236,17 +173,28 @@ export function runTeamPreflight({ root, args, config, policyDelta, teamBase })
|
|
|
236
173
|
return { halt: null, teamParliament: null, changedPaths: [] };
|
|
237
174
|
}
|
|
238
175
|
const againstRef = args.against || teamBase;
|
|
239
|
-
if (args.local && !againstRef) {
|
|
240
|
-
const
|
|
241
|
-
'
|
|
242
|
-
|
|
176
|
+
if ((args.local || args.changed || args.contractDiff) && !againstRef) {
|
|
177
|
+
const reasonId = args.local
|
|
178
|
+
? 'local-needs-base'
|
|
179
|
+
: args.changed
|
|
180
|
+
? 'changed-needs-base'
|
|
181
|
+
: 'contract-diff-needs-base';
|
|
182
|
+
const message = args.local
|
|
183
|
+
? '--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.'
|
|
184
|
+
: args.changed
|
|
185
|
+
? changedNeedsBaseMessage(args)
|
|
186
|
+
: contractDiffNeedsBaseMessage(args);
|
|
187
|
+
const teamParliament = { deny: false, reasonId, message };
|
|
243
188
|
return {
|
|
244
189
|
halt: { exitCode: 2, message, teamParliament },
|
|
245
190
|
teamParliament,
|
|
246
191
|
changedPaths: [],
|
|
247
192
|
};
|
|
248
193
|
}
|
|
249
|
-
|
|
194
|
+
// No base ref means the change set was never computed: never report it as empty.
|
|
195
|
+
const listed = againstRef
|
|
196
|
+
? listChangedPaths(root, againstRef)
|
|
197
|
+
: { ok: false, paths: [], error: 'No git base ref resolved.' };
|
|
250
198
|
const changedPaths = listed.ok ? listed.paths : [];
|
|
251
199
|
const changeSet = classifyChangeSet(changedPaths);
|
|
252
200
|
const baseBaselineRaw = againstRef
|
|
@@ -279,8 +227,12 @@ export function runTeamPreflight({ root, args, config, policyDelta, teamBase })
|
|
|
279
227
|
changedPathError: listed.ok ? null : listed.error,
|
|
280
228
|
};
|
|
281
229
|
if (!listed.ok && (args.changed || args.against || args.contractDiff)) {
|
|
230
|
+
// The verdict above ran over an uncomputed diff: never label it `ok`.
|
|
231
|
+
teamParliament.deny = false;
|
|
232
|
+
teamParliament.reasonId = 'changed-paths-unavailable';
|
|
233
|
+
teamParliament.message = `${listed.error || 'Cannot list changed paths.'} Pass --base <ref> or fix the git error, or run a full check without diff flags.`;
|
|
282
234
|
return {
|
|
283
|
-
halt: { exitCode: 2, message:
|
|
235
|
+
halt: { exitCode: 2, message: teamParliament.message, teamParliament },
|
|
284
236
|
teamParliament,
|
|
285
237
|
changedPaths,
|
|
286
238
|
};
|
|
@@ -302,6 +254,17 @@ export function runTeamPreflight({ root, args, config, policyDelta, teamBase })
|
|
|
302
254
|
return { halt: null, teamParliament, changedPaths };
|
|
303
255
|
}
|
|
304
256
|
|
|
257
|
+
/** `--changed` / `--persona touch|contributor|agent` without a resolvable merge base. */
|
|
258
|
+
function changedNeedsBaseMessage(args) {
|
|
259
|
+
const flag = args?.persona ? `--persona ${args.persona}` : '--changed';
|
|
260
|
+
return `${flag} needs a git merge base to know which files changed, and none was found (tried ${TEAM_BASE_CANDIDATES.join(', ')}; or this is not a git repository). Pass --base <ref> (for example --base HEAD or --base origin/main), or run without ${flag} for a full-tree check.`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function contractDiffNeedsBaseMessage(args) {
|
|
264
|
+
const flag = args?.persona ? `--persona ${args.persona}` : '--contract-diff';
|
|
265
|
+
return `${flag} compares the contract and .ark-baseline.json against a git base, and none was found (tried ${TEAM_BASE_CANDIDATES.join(', ')}; or this is not a git repository). Pass --base <ref> (for example --base origin/main), or run a plain full-tree check without ${flag}.`;
|
|
266
|
+
}
|
|
267
|
+
|
|
305
268
|
export function ungovernedDumpMessage(dumped) {
|
|
306
269
|
return `New ungoverned source in this diff: ${dumped.slice(0, 8).join(', ')}${dumped.length > 8 ? '…' : ''}. Classify via /ark-adopt (contract session) or move into a governed layer.`;
|
|
307
270
|
}
|
|
@@ -312,6 +275,29 @@ export function filterChangedGovernedFiles(allGovernedFiles, root, changedPaths,
|
|
|
312
275
|
return allGovernedFiles.filter((abs) => changedSet.has(normalizeRel(path.relative(root, abs))));
|
|
313
276
|
}
|
|
314
277
|
|
|
278
|
+
/**
|
|
279
|
+
* `--changed` scans a subset, so a layer whose files were not touched looks empty to the
|
|
280
|
+
* kernel. Keep CONFIG_LAYER_PATTERN_NO_MATCHES only when the FULL governed tree has no
|
|
281
|
+
* match either (a real typo). `loadFullFiles` is lazy: the full walk runs only when needed.
|
|
282
|
+
*/
|
|
283
|
+
export function pruneScopedPatternWarnings(warnings, root, loadFullFiles) {
|
|
284
|
+
const isNoMatch = (w) =>
|
|
285
|
+
(w?.ruleId ?? w?.code) === 'CONFIG_LAYER_PATTERN_NO_MATCHES' && typeof w.pattern === 'string';
|
|
286
|
+
if (!Array.isArray(warnings) || !warnings.some(isNoMatch)) return warnings;
|
|
287
|
+
const rels = loadFullFiles().map((abs) => path.relative(root, abs).split(path.sep).join('/'));
|
|
288
|
+
for (let i = warnings.length - 1; i >= 0; i -= 1) {
|
|
289
|
+
if (!isNoMatch(warnings[i])) continue;
|
|
290
|
+
let expression;
|
|
291
|
+
try {
|
|
292
|
+
expression = globToRegExp(warnings[i].pattern);
|
|
293
|
+
} catch {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (rels.some((rel) => expression.test(rel))) warnings.splice(i, 1);
|
|
297
|
+
}
|
|
298
|
+
return warnings;
|
|
299
|
+
}
|
|
300
|
+
|
|
315
301
|
export function applyAgainstRatchet({
|
|
316
302
|
violations,
|
|
317
303
|
againstRef,
|
|
@@ -319,16 +305,18 @@ export function applyAgainstRatchet({
|
|
|
319
305
|
changed,
|
|
320
306
|
changedPaths,
|
|
321
307
|
occurrenceKeys,
|
|
308
|
+
rules,
|
|
309
|
+
layers,
|
|
322
310
|
}) {
|
|
323
311
|
const baseRaw = readJsonMaybe(gitShowText(root, againstRef, '.ark-baseline.json'));
|
|
324
312
|
const baseKeys = new Set(baselineKeysFromDocument(baseRaw));
|
|
325
|
-
const judged = baseRaw ? applyAdvisorySiblingRatchet(violations, occurrenceKeys, baseKeys) : violations;
|
|
313
|
+
const judged = baseRaw ? applyAdvisorySiblingRatchet(violations, occurrenceKeys, baseKeys, { rules, layers }) : violations;
|
|
326
314
|
const vsBaseActive = judged.filter((_, index) => !baseKeys.has(occurrenceKeys[index]));
|
|
327
315
|
const changedSet = new Set(changedPaths ?? []);
|
|
316
|
+
// A CROSS_PARENT_VIA_SHARED path belongs to the change when any file on it changed.
|
|
317
|
+
const touched = (v) => [v.file, ...(Array.isArray(v.via) ? v.via : []), ...(v.reasonId === 'CROSS_PARENT_VIA_SHARED' ? [v.target] : [])];
|
|
328
318
|
const activeViolations = changed
|
|
329
|
-
? vsBaseActive.filter((
|
|
330
|
-
changedSet.has(String(violation.file || '').replace(/\\/g, '/'))
|
|
331
|
-
)
|
|
319
|
+
? vsBaseActive.filter((v) => touched(v).some((f) => changedSet.has(String(f || '').replace(/\\/g, '/'))))
|
|
332
320
|
: vsBaseActive;
|
|
333
321
|
return {
|
|
334
322
|
activeViolations,
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* Pure CLI helper (bin/lib/team-parliament.mjs). Zero Node I/O.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const TEAM_PERSONAS = ['touch', 'contributor', 'agent', 'steward'];
|
|
12
|
+
const CONTRACT_DIFF_KINDS = [
|
|
13
13
|
'unchanged',
|
|
14
14
|
'tighten',
|
|
15
15
|
'loosen',
|
|
@@ -35,11 +35,14 @@ export function isLawRelativePath(relPath) {
|
|
|
35
35
|
if ((n.startsWith('arkrules/') || n.includes('/arkrules/')) && n.endsWith('.json')) {
|
|
36
36
|
return true;
|
|
37
37
|
}
|
|
38
|
+
// Slice file named by childSlices.arkRulesFile (`arkrules.<Layer>.json`), including a lookalike.
|
|
39
|
+
if (/^arkrules\.[^/]+\.json$/i.test(base))
|
|
40
|
+
return true;
|
|
38
41
|
return false;
|
|
39
42
|
}
|
|
40
43
|
const PRODUCT_SOURCE = /\.(tsx?|jsx?|mjs|cjs)$/i;
|
|
41
44
|
/** Governable product source (not law, not tests-only heuristic — basename extension). */
|
|
42
|
-
|
|
45
|
+
function isProductSourceRelativePath(relPath) {
|
|
43
46
|
const n = posixRel(relPath);
|
|
44
47
|
if (!n || isLawRelativePath(n))
|
|
45
48
|
return false;
|
|
@@ -100,29 +103,34 @@ export function classifyBaselineKeyDelta(baseKeys, candidateKeys) {
|
|
|
100
103
|
kinds.push('baseline-shrink');
|
|
101
104
|
return { grow, shrink, kinds };
|
|
102
105
|
}
|
|
103
|
-
|
|
106
|
+
function normalizeStewardId(value) {
|
|
104
107
|
if (typeof value !== 'string')
|
|
105
108
|
return null;
|
|
106
109
|
const trimmed = value.trim().replace(/^@/, '').toLowerCase();
|
|
107
110
|
return trimmed.length > 0 ? trimmed : null;
|
|
108
111
|
}
|
|
109
112
|
/** GitHub handle (login), not a display name. Spaces and emails are not handles. */
|
|
110
|
-
|
|
113
|
+
function isGitHubHandle(value) {
|
|
111
114
|
const id = normalizeStewardId(value);
|
|
112
115
|
if (!id || isAutomationAuthor(id) || id.includes(' ') || id.includes('@'))
|
|
113
116
|
return false;
|
|
114
117
|
return /^(?!-)[a-z0-9-]{1,39}(?<!-)$/.test(id);
|
|
115
118
|
}
|
|
116
|
-
|
|
119
|
+
function isGitHubEmail(value) {
|
|
117
120
|
if (typeof value !== 'string')
|
|
118
121
|
return false;
|
|
119
122
|
const email = value.trim().toLowerCase();
|
|
120
123
|
if (!email.includes('@') || email.includes(' ') || isAutomationAuthor(email))
|
|
121
124
|
return false;
|
|
122
|
-
|
|
125
|
+
// local@domain.tld without a backtracking regex (CodeQL js/polynomial-redos).
|
|
126
|
+
const parts = email.split('@');
|
|
127
|
+
if (parts.length !== 2 || !parts[0] || /\s/.test(email))
|
|
128
|
+
return false;
|
|
129
|
+
const dot = parts[1].lastIndexOf('.');
|
|
130
|
+
return dot > 0 && dot < parts[1].length - 1;
|
|
123
131
|
}
|
|
124
132
|
/** `123+login@users.noreply.github.com` or `login@users.noreply.github.com` → login. */
|
|
125
|
-
|
|
133
|
+
function githubHandleFromEmail(email) {
|
|
126
134
|
if (typeof email !== 'string')
|
|
127
135
|
return null;
|
|
128
136
|
const match = email.trim().match(/^(?:\d+\+)?([^@]+)@users\.noreply\.github\.com$/i);
|
|
@@ -145,7 +153,7 @@ export function canonicalStewardId(value) {
|
|
|
145
153
|
return id;
|
|
146
154
|
return null;
|
|
147
155
|
}
|
|
148
|
-
|
|
156
|
+
function formatStewardMention(id) {
|
|
149
157
|
return id.includes('@') ? id : `@${id}`;
|
|
150
158
|
}
|
|
151
159
|
/**
|
|
@@ -173,7 +181,7 @@ export function isSteward(author, stewards) {
|
|
|
173
181
|
return stewards.some((entry) => canonicalStewardId(entry) === who);
|
|
174
182
|
}
|
|
175
183
|
const BOT_STEWARD = /bot\b|\[bot\]|dependabot|renovate|github-actions|imgbot|codecov/i;
|
|
176
|
-
|
|
184
|
+
function isAutomationAuthor(value) {
|
|
177
185
|
const id = normalizeStewardId(value);
|
|
178
186
|
return !id || BOT_STEWARD.test(id);
|
|
179
187
|
}
|
|
@@ -313,6 +321,9 @@ export function personaCheckBudget(persona) {
|
|
|
313
321
|
denyLoosenUnlessSteward: true,
|
|
314
322
|
};
|
|
315
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Narrow a string to a team persona.
|
|
326
|
+
*/
|
|
316
327
|
export function isTeamPersona(value) {
|
|
317
328
|
return TEAM_PERSONAS.includes(value);
|
|
318
329
|
}
|
|
@@ -46,7 +46,7 @@ function versionParts(version) {
|
|
|
46
46
|
* @param {string} right
|
|
47
47
|
* @returns {-1|0|1}
|
|
48
48
|
*/
|
|
49
|
-
|
|
49
|
+
function compareUpgradeVersions(left, right) {
|
|
50
50
|
const a = versionParts(left);
|
|
51
51
|
const b = versionParts(right);
|
|
52
52
|
const length = Math.max(a.length, b.length);
|
|
@@ -61,7 +61,7 @@ export function compareUpgradeVersions(left, right) {
|
|
|
61
61
|
* @param {UpgradeBehaviorChange} row
|
|
62
62
|
* @returns {string}
|
|
63
63
|
*/
|
|
64
|
-
|
|
64
|
+
function formatBehaviorChange(row) {
|
|
65
65
|
return `${row.version}: ${row.note}`;
|
|
66
66
|
}
|
|
67
67
|
|