arkgate 4.8.24 → 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 +85 -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 +55 -8
- 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 +31 -62
- 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-AHACEKSV.js +16 -0
- package/dist/chunk-UN5EH4QV.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 +2 -1
- package/docs/agent-guide.md +134 -2
- package/docs/ai-gates.md +15 -1
- package/docs/configuration.md +40 -11
- package/docs/diagnostics.md +118 -2
- 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
|
@@ -7,7 +7,6 @@ import os from 'node:os';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { arkCommand } from '../ark-shared.mjs';
|
|
9
9
|
import {
|
|
10
|
-
parseSkillDescriptionVersion,
|
|
11
10
|
stampSkillDescription,
|
|
12
11
|
stripSkillDescriptionVersion,
|
|
13
12
|
} from './agent-skills-package.mjs';
|
|
@@ -227,14 +226,14 @@ export function canonicalSkillPath(name) {
|
|
|
227
226
|
* Hosts that natively load `.agents/skills` — do not also copy bytes there under
|
|
228
227
|
* a second name. Cursor/Codex list every path they scan; two copies = two picker rows.
|
|
229
228
|
*/
|
|
230
|
-
|
|
229
|
+
const SKILL_NATIVE_AGENTS_HOSTS = Object.freeze(['codex', 'cursor', 'antigravity']);
|
|
231
230
|
|
|
232
231
|
/**
|
|
233
232
|
* Hosts that do not scan `.agents/skills`. Adapter is a relative symlink to the
|
|
234
233
|
* canonical catalog so Grok/Claude/OpenCode see the same bytes.
|
|
235
234
|
* Cursor also scans `.claude/skills` — doctor warns; still one body + visible version.
|
|
236
235
|
*/
|
|
237
|
-
|
|
236
|
+
const SKILL_ADAPTER_LINKS = Object.freeze({
|
|
238
237
|
claude: (name) => ({
|
|
239
238
|
link: `.claude/skills/${name}`,
|
|
240
239
|
target: `../../${SKILL_CANONICAL_DIR}/${name}`,
|
|
@@ -299,10 +298,10 @@ export function arkPackageVersion() {
|
|
|
299
298
|
}
|
|
300
299
|
|
|
301
300
|
/**
|
|
302
|
-
* Rewrite a YAML `description:` line
|
|
303
|
-
*
|
|
301
|
+
* Rewrite the value of a YAML `description:` line, preserving its quoting.
|
|
302
|
+
* `transform` returns the new value, or null to keep the line untouched.
|
|
304
303
|
*/
|
|
305
|
-
function
|
|
304
|
+
function rewriteDescriptionYamlLine(line, transform) {
|
|
306
305
|
const match = String(line).match(/^(description:\s*)(.*)$/);
|
|
307
306
|
if (!match) return line;
|
|
308
307
|
let raw = match[2] ?? '';
|
|
@@ -311,26 +310,26 @@ function stampDescriptionYamlLine(line, version) {
|
|
|
311
310
|
(raw.startsWith("'") && raw.endsWith("'") && raw.length >= 2);
|
|
312
311
|
const quote = quoted ? raw[0] : '';
|
|
313
312
|
const value = quoted ? raw.slice(1, -1) : raw;
|
|
314
|
-
const
|
|
315
|
-
if (
|
|
316
|
-
|
|
313
|
+
const next = transform(value);
|
|
314
|
+
if (next === null) return line;
|
|
315
|
+
if (!quoted) return `${match[1]}${next}`;
|
|
316
|
+
const escaped = next.replaceAll('\\', '\\\\').replaceAll(quote, `\\${quote}`);
|
|
317
317
|
return `${match[1]}${quote}${escaped}${quote}`;
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Rewrite a YAML `description:` line with a visible `arkgate@<version>. ` prefix.
|
|
322
|
+
* Preserves quoting. Hosts show this string in the skill picker (unlike arkVersion).
|
|
323
|
+
*/
|
|
324
|
+
function stampDescriptionYamlLine(line, version) {
|
|
325
|
+
return rewriteDescriptionYamlLine(line, (value) => stampSkillDescription(value, version));
|
|
326
|
+
}
|
|
327
|
+
|
|
320
328
|
function managedDescriptionYamlLine(line) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
(raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) ||
|
|
326
|
-
(raw.startsWith("'") && raw.endsWith("'") && raw.length >= 2);
|
|
327
|
-
const quote = quoted ? raw[0] : '';
|
|
328
|
-
const value = quoted ? raw.slice(1, -1) : raw;
|
|
329
|
-
const stripped = stripSkillDescriptionVersion(value);
|
|
330
|
-
if (stripped === value) return line;
|
|
331
|
-
if (!quoted) return `${match[1]}${stripped}`;
|
|
332
|
-
const escaped = stripped.replaceAll('\\', '\\\\').replaceAll(quote, `\\${quote}`);
|
|
333
|
-
return `${match[1]}${quote}${escaped}${quote}`;
|
|
329
|
+
return rewriteDescriptionYamlLine(line, (value) => {
|
|
330
|
+
const stripped = stripSkillDescriptionVersion(value);
|
|
331
|
+
return stripped === value ? null : stripped;
|
|
332
|
+
});
|
|
334
333
|
}
|
|
335
334
|
|
|
336
335
|
// Insert `arkVersion: <v>` and a visible `arkgate@<v>. ` description prefix.
|
|
@@ -593,7 +592,7 @@ export function skillTemplates() {
|
|
|
593
592
|
* Relative symlink so clones keep working. Fallback copy when the OS refuses links.
|
|
594
593
|
* @returns {'linked'|'copied'|'current'|'skipped-customized'|'missing-canonical'}
|
|
595
594
|
*/
|
|
596
|
-
|
|
595
|
+
function ensureSkillAdapterLink(root, name, adapter, force = false) {
|
|
597
596
|
const canonicalDir = path.join(root, SKILL_CANONICAL_DIR, name);
|
|
598
597
|
const canonicalFile = path.join(canonicalDir, 'SKILL.md');
|
|
599
598
|
if (!fs.existsSync(canonicalFile)) return 'missing-canonical';
|
|
@@ -693,8 +692,6 @@ export function pruneHomeArkSkillDuplicates(root, skillNames = skillTemplateName
|
|
|
693
692
|
return { ok: true, reason: 'pruned', removed };
|
|
694
693
|
}
|
|
695
694
|
|
|
696
|
-
export { parseSkillDescriptionVersion, stripSkillDescriptionVersion };
|
|
697
|
-
|
|
698
695
|
// Skill names only, silent on a missing templates dir — for the freshness
|
|
699
696
|
// advisory below, which must not print packaging warnings on every check run.
|
|
700
697
|
export function skillTemplateNames() {
|
|
@@ -821,20 +818,15 @@ function readCodexHomeCatalogMetadata(file, kind) {
|
|
|
821
818
|
}
|
|
822
819
|
}
|
|
823
820
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
'pending'
|
|
832
|
-
);
|
|
821
|
+
/**
|
|
822
|
+
* Merge a home skill catalog and its pending catalog metadata: the floor is the
|
|
823
|
+
* newer of the two versions; either file present means metadata is in play.
|
|
824
|
+
* @param {{ exists: boolean, valid: boolean, version: string|null }} catalog
|
|
825
|
+
* @param {{ exists: boolean, valid: boolean, version: string|null }} pending
|
|
826
|
+
*/
|
|
827
|
+
export function homeCatalogFloorState(catalog, pending) {
|
|
833
828
|
let floorVersion = catalog.version;
|
|
834
|
-
if (
|
|
835
|
-
pending.version &&
|
|
836
|
-
(!floorVersion || isVersionOlder(floorVersion, pending.version))
|
|
837
|
-
) {
|
|
829
|
+
if (pending.version && (!floorVersion || isVersionOlder(floorVersion, pending.version))) {
|
|
838
830
|
floorVersion = pending.version;
|
|
839
831
|
}
|
|
840
832
|
return {
|
|
@@ -846,6 +838,18 @@ function codexHomeCatalogState(skillsDir) {
|
|
|
846
838
|
};
|
|
847
839
|
}
|
|
848
840
|
|
|
841
|
+
function codexHomeCatalogState(skillsDir) {
|
|
842
|
+
const catalog = readCodexHomeCatalogMetadata(
|
|
843
|
+
path.join(skillsDir, CODEX_HOME_CATALOG),
|
|
844
|
+
'catalog'
|
|
845
|
+
);
|
|
846
|
+
const pending = readCodexHomeCatalogMetadata(
|
|
847
|
+
path.join(skillsDir, CODEX_HOME_PENDING_CATALOG),
|
|
848
|
+
'pending'
|
|
849
|
+
);
|
|
850
|
+
return homeCatalogFloorState(catalog, pending);
|
|
851
|
+
}
|
|
852
|
+
|
|
849
853
|
function newerCodexHomeCatalogVersion(skillsDir, packageVersion, state = null) {
|
|
850
854
|
if (!isValidSemver(packageVersion)) return null;
|
|
851
855
|
const floorVersion = (state ?? codexHomeCatalogState(skillsDir)).floorVersion;
|
|
@@ -12,7 +12,7 @@ import { getDiagnosticCatalogEntry } from './diagnostic-catalog.mjs';
|
|
|
12
12
|
import { demoteExtraPlaneTeethUnderClassificationFloor } from './extra-merge-teeth.mjs';
|
|
13
13
|
|
|
14
14
|
/** Hook-only guidance: a hook deny is already the verdict for that write. */
|
|
15
|
-
|
|
15
|
+
const HOOK_DENY_PREPARE_CHANGE_NOTE = 'Do not call ark_prepare_change from a hook deny.';
|
|
16
16
|
|
|
17
17
|
function surfaceNote(context) {
|
|
18
18
|
return context?.surface === 'hook' ? ` ${HOOK_DENY_PREPARE_CHANGE_NOTE}` : '';
|
|
@@ -35,7 +35,7 @@ function hostOutputTail(hostOutput) {
|
|
|
35
35
|
* @param {string|undefined} installCommand
|
|
36
36
|
* @returns {string|null}
|
|
37
37
|
*/
|
|
38
|
-
|
|
38
|
+
function resolveStartInstallPackagePin(packageVersion, installCommand) {
|
|
39
39
|
const fromArg = String(packageVersion ?? '')
|
|
40
40
|
.trim()
|
|
41
41
|
.replace(/^arkgate@/i, '')
|
|
@@ -147,7 +147,7 @@ export function setupBudget(changes) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/** One next write when compact start is the wrong size. `--force` does not unlock. */
|
|
150
|
-
|
|
150
|
+
const START_SETUP_BUDGET_NEXT = 'arkgate-check --init';
|
|
151
151
|
|
|
152
152
|
function formatSetupKb(bytes) {
|
|
153
153
|
return `${Math.max(1, Math.round(Number(bytes) / 1024))} KB`;
|
|
@@ -9,7 +9,7 @@ import fs from 'node:fs';
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
|
|
11
11
|
/** Product-domain / domain-model homes. Closed list — no tree walk. */
|
|
12
|
-
|
|
12
|
+
const STATES_TRANSITIONS_DOMAIN_HOMES = Object.freeze([
|
|
13
13
|
'docs/domain.md',
|
|
14
14
|
'docs/data-model.md',
|
|
15
15
|
'docs/product-domain.md',
|
|
@@ -19,7 +19,7 @@ export const STATES_TRANSITIONS_DOMAIN_HOMES = Object.freeze([
|
|
|
19
19
|
]);
|
|
20
20
|
|
|
21
21
|
/** Evolved dedicated homes. Adopt if present; do not force a filename. */
|
|
22
|
-
|
|
22
|
+
const STATES_TRANSITIONS_DEDICATED_HOMES = Object.freeze([
|
|
23
23
|
'docs/states.md',
|
|
24
24
|
'docs/transitions.md',
|
|
25
25
|
'docs/states-transitions.md',
|
|
@@ -32,7 +32,7 @@ export const STATES_TRANSITIONS_ASK_MISSING =
|
|
|
32
32
|
export const STATES_TRANSITIONS_ASK_INCOMPLETE =
|
|
33
33
|
'The states heading is there, but it is not a map yet — no table or link.';
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
const STATES_TRANSITIONS_NEXT =
|
|
36
36
|
'Add a short table (entity · states · allowed from → to) — or one link — on the domain doc. Names from the code. No flag soup.';
|
|
37
37
|
|
|
38
38
|
function readMarkdown(abs) {
|
|
@@ -30,6 +30,7 @@ import { projectStatusArkRun } from './ark-run-doctor.mjs';
|
|
|
30
30
|
import { projectStatusArkOrder } from './ark-order-doctor.mjs';
|
|
31
31
|
import { collectVsBaseFacts, discoverTeamBaseRef } from './team-parliament-io.mjs';
|
|
32
32
|
import { classifyAdopted, readAdoptionStance } from './adoption-stance.mjs';
|
|
33
|
+
import { lastCheckVerdictFacts } from './upstream-report.mjs';
|
|
33
34
|
|
|
34
35
|
function sha256Hex(value) {
|
|
35
36
|
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
@@ -115,24 +116,7 @@ export function lastCheckFactsFromSnapshot(latest, baseline) {
|
|
|
115
116
|
frozenResidual,
|
|
116
117
|
};
|
|
117
118
|
}
|
|
118
|
-
const at =
|
|
119
|
-
typeof latest.generatedAt === 'string'
|
|
120
|
-
? latest.generatedAt
|
|
121
|
-
: typeof latest.at === 'string'
|
|
122
|
-
? latest.at
|
|
123
|
-
: null;
|
|
124
|
-
const active =
|
|
125
|
-
typeof latest.activeViolations === 'number'
|
|
126
|
-
? latest.activeViolations
|
|
127
|
-
: typeof latest.violations?.active === 'number'
|
|
128
|
-
? latest.violations.active
|
|
129
|
-
: null;
|
|
130
|
-
let verdict = null;
|
|
131
|
-
if (latest.ok === true && (active == null || active === 0)) verdict = 'pass';
|
|
132
|
-
else if (latest.ok === false || (typeof active === 'number' && active > 0)) verdict = 'fail';
|
|
133
|
-
else if (latest.completeness === 'partial' || latest.completeness === 'unavailable') {
|
|
134
|
-
verdict = 'incomplete';
|
|
135
|
-
} else if (latest.ok === true) verdict = 'pass';
|
|
119
|
+
const { at, active, verdict } = lastCheckVerdictFacts(latest);
|
|
136
120
|
return {
|
|
137
121
|
lastCheckAt: at,
|
|
138
122
|
lastCheckVerdict: verdict,
|
|
@@ -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
|
/**
|
|
@@ -653,31 +669,7 @@ export const ARK_STATUS_MANIFEST_SCHEMA = {
|
|
|
653
669
|
baselineGrew: { type: 'boolean' },
|
|
654
670
|
},
|
|
655
671
|
},
|
|
656
|
-
arkRun:
|
|
657
|
-
|
|
658
|
-
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.',
|
|
659
|
-
additionalProperties: false,
|
|
660
|
-
required: ['notAScore', 'present', 'mode', 'extraMergeTeeth', 'residual'],
|
|
661
|
-
properties: {
|
|
662
|
-
notAScore: { const: true },
|
|
663
|
-
present: { type: 'boolean' },
|
|
664
|
-
mode: { anyOf: [{ enum: ['advisory', 'enforced'] }, { type: 'null' }] },
|
|
665
|
-
extraMergeTeeth: { type: 'boolean' },
|
|
666
|
-
residual: { anyOf: [{ type: 'integer', minimum: 0 }, { type: 'null' }] },
|
|
667
|
-
},
|
|
668
|
-
},
|
|
669
|
-
arkOrder: {
|
|
670
|
-
type: 'object',
|
|
671
|
-
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.',
|
|
672
|
-
additionalProperties: false,
|
|
673
|
-
required: ['notAScore', 'present', 'mode', 'extraMergeTeeth', 'residual'],
|
|
674
|
-
properties: {
|
|
675
|
-
notAScore: { const: true },
|
|
676
|
-
present: { type: 'boolean' },
|
|
677
|
-
mode: { anyOf: [{ enum: ['advisory', 'enforced'] }, { type: 'null' }] },
|
|
678
|
-
extraMergeTeeth: { type: 'boolean' },
|
|
679
|
-
residual: { anyOf: [{ type: 'integer', minimum: 0 }, { type: 'null' }] },
|
|
680
|
-
},
|
|
681
|
-
},
|
|
672
|
+
arkRun: extraPlaneStatusSchema('ArkRun'),
|
|
673
|
+
arkOrder: extraPlaneStatusSchema('ArkOrder'),
|
|
682
674
|
},
|
|
683
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) => {
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
suggestStewards,
|
|
21
21
|
} from './team-parliament.mjs';
|
|
22
22
|
import {
|
|
23
|
-
SPAWN_TIMEOUT_MS,
|
|
24
23
|
TEAM_BASE_CANDIDATES,
|
|
25
24
|
discoverTeamBaseRef,
|
|
26
25
|
gitShowText,
|
|
@@ -29,21 +28,8 @@ import {
|
|
|
29
28
|
safeGitRef,
|
|
30
29
|
} from './git-change-scope.mjs';
|
|
31
30
|
|
|
32
|
-
export {
|
|
33
|
-
baselineKeysFromDocument,
|
|
34
|
-
classifyBaselineKeyDelta,
|
|
35
|
-
classifyChangeSet,
|
|
36
|
-
evaluateTeamGate,
|
|
37
|
-
formatVsBaseLine,
|
|
38
|
-
isTeamPersona,
|
|
39
|
-
mapPolicyClassToKind,
|
|
40
|
-
parseCodeownersHandles,
|
|
41
|
-
personaCheckBudget,
|
|
42
|
-
resolveStewardHandle,
|
|
43
|
-
suggestStewards,
|
|
44
|
-
};
|
|
45
31
|
// prettier-ignore
|
|
46
|
-
export {
|
|
32
|
+
export { discoverTeamBaseRef, safeGitRef };
|
|
47
33
|
|
|
48
34
|
export function contractSessionFrom(args, env = process.env) {
|
|
49
35
|
if (args?.contractSession === true) return true;
|
|
@@ -269,12 +255,12 @@ export function runTeamPreflight({ root, args, config, policyDelta, teamBase })
|
|
|
269
255
|
}
|
|
270
256
|
|
|
271
257
|
/** `--changed` / `--persona touch|contributor|agent` without a resolvable merge base. */
|
|
272
|
-
|
|
258
|
+
function changedNeedsBaseMessage(args) {
|
|
273
259
|
const flag = args?.persona ? `--persona ${args.persona}` : '--changed';
|
|
274
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.`;
|
|
275
261
|
}
|
|
276
262
|
|
|
277
|
-
|
|
263
|
+
function contractDiffNeedsBaseMessage(args) {
|
|
278
264
|
const flag = args?.persona ? `--persona ${args.persona}` : '--contract-diff';
|
|
279
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}.`;
|
|
280
266
|
}
|
|
@@ -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,20 +103,20 @@ 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();
|
|
@@ -127,7 +130,7 @@ export function isGitHubEmail(value) {
|
|
|
127
130
|
return dot > 0 && dot < parts[1].length - 1;
|
|
128
131
|
}
|
|
129
132
|
/** `123+login@users.noreply.github.com` or `login@users.noreply.github.com` → login. */
|
|
130
|
-
|
|
133
|
+
function githubHandleFromEmail(email) {
|
|
131
134
|
if (typeof email !== 'string')
|
|
132
135
|
return null;
|
|
133
136
|
const match = email.trim().match(/^(?:\d+\+)?([^@]+)@users\.noreply\.github\.com$/i);
|
|
@@ -150,7 +153,7 @@ export function canonicalStewardId(value) {
|
|
|
150
153
|
return id;
|
|
151
154
|
return null;
|
|
152
155
|
}
|
|
153
|
-
|
|
156
|
+
function formatStewardMention(id) {
|
|
154
157
|
return id.includes('@') ? id : `@${id}`;
|
|
155
158
|
}
|
|
156
159
|
/**
|
|
@@ -178,7 +181,7 @@ export function isSteward(author, stewards) {
|
|
|
178
181
|
return stewards.some((entry) => canonicalStewardId(entry) === who);
|
|
179
182
|
}
|
|
180
183
|
const BOT_STEWARD = /bot\b|\[bot\]|dependabot|renovate|github-actions|imgbot|codecov/i;
|
|
181
|
-
|
|
184
|
+
function isAutomationAuthor(value) {
|
|
182
185
|
const id = normalizeStewardId(value);
|
|
183
186
|
return !id || BOT_STEWARD.test(id);
|
|
184
187
|
}
|
|
@@ -318,6 +321,9 @@ export function personaCheckBudget(persona) {
|
|
|
318
321
|
denyLoosenUnlessSteward: true,
|
|
319
322
|
};
|
|
320
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Narrow a string to a team persona.
|
|
326
|
+
*/
|
|
321
327
|
export function isTeamPersona(value) {
|
|
322
328
|
return TEAM_PERSONAS.includes(value);
|
|
323
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
|
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
upgradeOutcome,
|
|
19
19
|
} from './upgrade-outcome.mjs';
|
|
20
20
|
import { __packageRoot } from './gate-files.mjs';
|
|
21
|
+
import { compareSemverCore } from './upgrade-package-decision.mjs';
|
|
21
22
|
import {
|
|
22
23
|
applyManagedUpgrade,
|
|
23
24
|
managedUpgradeJson,
|
|
@@ -51,30 +52,7 @@ export function resolveProjectArkgatePackageJson(root) {
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
* Compare numeric major.minor.patch cores (prerelease / build ignored).
|
|
56
|
-
* @returns {-1|0|1}
|
|
57
|
-
*/
|
|
58
|
-
export function compareSemverCore(a, b) {
|
|
59
|
-
const parse = (value) => {
|
|
60
|
-
const core = String(value ?? '')
|
|
61
|
-
.trim()
|
|
62
|
-
.replace(/^v/i, '')
|
|
63
|
-
.split(/[-+]/)[0];
|
|
64
|
-
const parts = core.split('.').map((part) => {
|
|
65
|
-
const n = Number.parseInt(part, 10);
|
|
66
|
-
return Number.isFinite(n) ? n : 0;
|
|
67
|
-
});
|
|
68
|
-
return [parts[0] || 0, parts[1] || 0, parts[2] || 0];
|
|
69
|
-
};
|
|
70
|
-
const left = parse(a);
|
|
71
|
-
const right = parse(b);
|
|
72
|
-
for (let i = 0; i < 3; i += 1) {
|
|
73
|
-
if (left[i] < right[i]) return -1;
|
|
74
|
-
if (left[i] > right[i]) return 1;
|
|
75
|
-
}
|
|
76
|
-
return 0;
|
|
77
|
-
}
|
|
55
|
+
export { compareSemverCore };
|
|
78
56
|
|
|
79
57
|
/** True when candidate is the same path as root or a descendant (after resolve). */
|
|
80
58
|
export function isPathInside(candidate, root) {
|
|
@@ -144,7 +144,7 @@ function sampleForViolation(violation, ruleId) {
|
|
|
144
144
|
* @param {string} stdout
|
|
145
145
|
* @returns {Record<string, unknown>|null}
|
|
146
146
|
*/
|
|
147
|
-
|
|
147
|
+
function parseCheckStdout(stdout) {
|
|
148
148
|
const text = typeof stdout === 'string' ? stdout.trim() : '';
|
|
149
149
|
if (!text) return null;
|
|
150
150
|
try {
|
|
@@ -10,7 +10,7 @@ import readline from 'node:readline/promises';
|
|
|
10
10
|
import { __packageRoot } from './gate-files.mjs';
|
|
11
11
|
|
|
12
12
|
export const UPSTREAM_OWNER_REPO = 'pedroknigge/arkgate';
|
|
13
|
-
|
|
13
|
+
const SUBMIT_PROMPT = 'Type submit to send';
|
|
14
14
|
|
|
15
15
|
export function ownerRepoFromGithubUrl(url) {
|
|
16
16
|
if (typeof url !== 'string' || url.trim() === '') return null;
|
|
@@ -22,7 +22,7 @@ export function ownerRepoFromGithubUrl(url) {
|
|
|
22
22
|
return `${owner}/${repo}`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
function resolveUpstreamRepo(pkg) {
|
|
26
26
|
const fromBugs = ownerRepoFromGithubUrl(pkg?.bugs?.url);
|
|
27
27
|
if (fromBugs) return fromBugs;
|
|
28
28
|
const repository = pkg?.repository;
|
|
@@ -32,7 +32,7 @@ export function resolveUpstreamRepo(pkg) {
|
|
|
32
32
|
return UPSTREAM_OWNER_REPO;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
function posixSingleQuote(value) {
|
|
36
36
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -40,7 +40,7 @@ export function formatGhIssueCreateCommand({ repo, title, body }) {
|
|
|
40
40
|
return `gh issue create --repo ${posixSingleQuote(repo)} --title ${posixSingleQuote(title)} --body ${posixSingleQuote(body)}`;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
function reportUsage() {
|
|
44
44
|
return `arkgate report (alias ark report) — draft an upstream GitHub issue for ArkGate.
|
|
45
45
|
|
|
46
46
|
Target: pedroknigge/arkgate (this package's package.json bugs.url). Never the consumer repo.
|
|
@@ -59,8 +59,13 @@ gh issue create --repo pedroknigge/arkgate command, then exits 2.
|
|
|
59
59
|
`;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Timestamp, active violation count, and verdict of a `.ark/reports/latest.json`
|
|
64
|
+
* snapshot object. Verdict: pass / fail / incomplete, or null when unknown.
|
|
65
|
+
* @param {object} latest
|
|
66
|
+
* @returns {{ at: string|null, active: number|null, verdict: 'pass'|'fail'|'incomplete'|null }}
|
|
67
|
+
*/
|
|
68
|
+
export function lastCheckVerdictFacts(latest) {
|
|
64
69
|
const at =
|
|
65
70
|
typeof latest.generatedAt === 'string'
|
|
66
71
|
? latest.generatedAt
|
|
@@ -79,11 +84,17 @@ export function lastCheckSnippet(latest) {
|
|
|
79
84
|
else if (latest.completeness === 'partial' || latest.completeness === 'unavailable') {
|
|
80
85
|
verdict = 'incomplete';
|
|
81
86
|
} else if (latest.ok === true) verdict = 'pass';
|
|
87
|
+
return { at, active, verdict };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function lastCheckSnippet(latest) {
|
|
91
|
+
if (!latest || typeof latest !== 'object') return null;
|
|
92
|
+
const { at, active, verdict } = lastCheckVerdictFacts(latest);
|
|
82
93
|
if (at == null && verdict == null && active == null) return null;
|
|
83
94
|
return { at, verdict, activeViolations: active };
|
|
84
95
|
}
|
|
85
96
|
|
|
86
|
-
|
|
97
|
+
function readLastCheckSnapshot(root) {
|
|
87
98
|
const latestPath = path.join(root, '.ark', 'reports', 'latest.json');
|
|
88
99
|
try {
|
|
89
100
|
return JSON.parse(fs.readFileSync(latestPath, 'utf8'));
|
|
@@ -92,7 +103,7 @@ export function readLastCheckSnapshot(root) {
|
|
|
92
103
|
}
|
|
93
104
|
}
|
|
94
105
|
|
|
95
|
-
|
|
106
|
+
function buildIssueDraft({ repo, arkgateVersion, lastCheck, finding, title }) {
|
|
96
107
|
const resolvedTitle =
|
|
97
108
|
typeof title === 'string' && title.trim()
|
|
98
109
|
? title.trim()
|
|
@@ -127,7 +138,7 @@ export function buildIssueDraft({ repo, arkgateVersion, lastCheck, finding, titl
|
|
|
127
138
|
return { repo, title: resolvedTitle, body: lines.join('\n') };
|
|
128
139
|
}
|
|
129
140
|
|
|
130
|
-
|
|
141
|
+
function defaultRunGh(argv, options = {}) {
|
|
131
142
|
const env = { ...(options.env ?? process.env) };
|
|
132
143
|
delete env.GH_REPO;
|
|
133
144
|
const result = spawnSync('gh', argv, {
|
|
@@ -182,7 +193,7 @@ async function defaultPromptSubmit(stdin, stdout) {
|
|
|
182
193
|
}
|
|
183
194
|
}
|
|
184
195
|
|
|
185
|
-
|
|
196
|
+
function parseReportArgv(argv = []) {
|
|
186
197
|
const out = {
|
|
187
198
|
submit: false,
|
|
188
199
|
iConfirmSubmit: false,
|