arkgate 4.5.7 → 4.6.1
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 +48 -1
- package/README.md +18 -12
- package/bin/ark-check-runtime.mjs +107 -8
- package/bin/ark-mcp-runtime.mjs +17 -1
- package/bin/ark.mjs +5 -3
- package/bin/lib/agent-homes.mjs +296 -0
- package/bin/lib/agent-projection.mjs +2 -2
- package/bin/lib/analysis-engine.mjs +3 -3
- package/bin/lib/ci-and-commands.mjs +16 -16
- package/bin/lib/config-contract.mjs +2 -0
- package/bin/lib/contract-smells.mjs +5 -5
- package/bin/lib/design-smells.mjs +4 -8
- package/bin/lib/doctor-advisories.mjs +9 -0
- package/bin/lib/doctor-plan.mjs +39 -15
- package/bin/lib/field-install.mjs +1 -1
- package/bin/lib/golden-pattern.mjs +1 -1
- package/bin/lib/host-support-matrix.mjs +4 -4
- package/bin/lib/html-report-advisories.mjs +20 -0
- package/bin/lib/html-report-depth.mjs +7 -8
- package/bin/lib/html-report.mjs +2 -1
- package/bin/lib/improvement-compass-map.mjs +20 -20
- package/bin/lib/install-migrate.mjs +12 -0
- package/bin/lib/pilot-loop.mjs +1 -1
- package/bin/lib/post-green-path.mjs +5 -4
- package/bin/lib/prepare-change.mjs +9 -0
- package/bin/lib/product-copy.mjs +32 -0
- package/bin/lib/skill-write.mjs +1 -1
- package/bin/lib/status-command.mjs +14 -0
- package/bin/lib/status-manifest.mjs +17 -0
- package/bin/lib/team-parliament-io.mjs +338 -0
- package/bin/lib/team-parliament.mjs +383 -0
- package/bin/lib/upgrade-whats-new.mjs +32 -0
- package/bin/lib/violations.mjs +8 -4
- package/dist/{configTypes-CC0FEXoF.d.ts → configTypes-B8uIcLaG.d.ts} +5 -0
- package/dist/eslint/index.cjs +2 -2
- package/dist/eslint/index.d.ts +1 -1
- package/dist/eslint/index.js +2 -2
- package/dist/index.cjs +8 -8
- package/dist/index.d.ts +61 -2
- package/dist/index.js +8 -8
- package/docs/README.md +5 -4
- package/docs/agent-guide.md +31 -24
- package/docs/ai-gates.md +19 -0
- package/docs/configuration.md +36 -2
- package/docs/develop.md +16 -4
- package/docs/enthusiast/README.md +1 -1
- package/docs/enthusiast/how-to-agent-gates.md +3 -3
- package/docs/enthusiast/how-to-pick-shape.md +2 -2
- package/docs/enthusiast/tutorial-first-project.md +4 -3
- package/docs/package-surface.md +5 -3
- package/docs/product-voice.md +126 -82
- package/docs/use.md +13 -6
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +9 -0
- package/schemas/ark.status-manifest.schema.json +51 -0
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +46 -19
- package/templates/agent-skills/ark-architect/SKILL.md +24 -145
- package/templates/agent-skills/ark-autopilot/SKILL.md +40 -30
- package/templates/agent-skills/ark-contract/SKILL.md +21 -105
- package/templates/agent-skills/ark-coverage/SKILL.md +3 -0
- package/templates/agent-skills/ark-explain/SKILL.md +7 -4
- package/templates/agent-skills/ark-explore/SKILL.md +29 -21
- package/templates/agent-skills/ark-fix/SKILL.md +34 -157
- package/templates/agent-skills/ark-loop/SKILL.md +31 -153
- package/templates/agent-skills/ark-place/SKILL.md +25 -13
- package/templates/agent-skills/ark-runtime/SKILL.md +3 -3
- package/templates/agent-skills/ark-think/SKILL.md +5 -2
- package/templates/agent-skills/ark-upgrade/SKILL.md +13 -8
- package/templates/skills/ark-adopt.md +46 -19
- package/templates/skills/ark-architect.md +24 -145
- package/templates/skills/ark-autopilot.md +40 -30
- package/templates/skills/ark-contract.md +21 -105
- package/templates/skills/ark-coverage.md +3 -0
- package/templates/skills/ark-explain.md +7 -4
- package/templates/skills/ark-explore.md +29 -21
- package/templates/skills/ark-fix.md +34 -157
- package/templates/skills/ark-loop.md +31 -153
- package/templates/skills/ark-place.md +25 -13
- package/templates/skills/ark-runtime.md +3 -3
- package/templates/skills/ark-think.md +5 -2
- package/templates/skills/ark-upgrade.md +13 -8
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-facing product copy (4.6). JSON field names stay stable; this module
|
|
3
|
+
* owns labels people read in doctor, HTML, and compact router.
|
|
4
|
+
*
|
|
5
|
+
* Brands kept: ArkGate, ArkRules. Dialect (design-weak, hard write, …) maps to
|
|
6
|
+
* common software words. See docs/product-voice.md.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Status-light leftover-design qualifier (was “design-weak”). */
|
|
10
|
+
export const LEFTOVER_DESIGN_LABEL = 'leftover design work';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Operating-mode title for humans (and doctor JSON `designFitness.label` prefix).
|
|
14
|
+
* @param {string|null|undefined} mode suggest|adapt|enforce
|
|
15
|
+
* @param {boolean} leftoverDesign
|
|
16
|
+
*/
|
|
17
|
+
export function operatingModeTitle(mode, leftoverDesign) {
|
|
18
|
+
const light = String(mode || 'enforce').toUpperCase();
|
|
19
|
+
return leftoverDesign ? `${light} · ${LEFTOVER_DESIGN_LABEL}` : light;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Short HTML/doctor badge text. */
|
|
23
|
+
export const LEFTOVER_DESIGN_BADGE = LEFTOVER_DESIGN_LABEL;
|
|
24
|
+
|
|
25
|
+
export const POST_GREEN_HUMAN =
|
|
26
|
+
'Imports check out, but the design is still messy. Map leftover work with /ark-explore shape-focus, then apply one small refactor via /ark-autopilot. A clean import check is not done; pattern bets are never auto-applied.';
|
|
27
|
+
|
|
28
|
+
export const POST_GREEN_LEDE =
|
|
29
|
+
'Import rules are clean, but leftover design work remains. That does not fail the check — it only means “done” is still wrong until you tidy shape.';
|
|
30
|
+
|
|
31
|
+
export const HARD_WRITE_HUMAN = 'pre-write block';
|
|
32
|
+
export const ADVISORY_WRITE_HUMAN = 'warning only (not blocked)';
|
package/bin/lib/skill-write.mjs
CHANGED
|
@@ -16,7 +16,7 @@ export const HOME_SKILL_PENDING_CATALOG = '.arkgate-catalog.pending.json';
|
|
|
16
16
|
const HOME_SKILL_LOCK = '.arkgate-install.lock';
|
|
17
17
|
const HOME_SKILL_CATALOG_SCHEMA = '1.0';
|
|
18
18
|
const HOME_SKILL_LOCK_STALE_MS = 5 * 60 * 1000;
|
|
19
|
-
const HOME_SKILL_LOCK_ATTEMPTS =
|
|
19
|
+
const HOME_SKILL_LOCK_ATTEMPTS = 200;
|
|
20
20
|
const HOME_SKILL_LOCK_RETRY_MS = 25;
|
|
21
21
|
const UUID_PATTERN =
|
|
22
22
|
/^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
|
|
@@ -25,6 +25,7 @@ import { detectActiveAgentHost } from './skill-install.mjs';
|
|
|
25
25
|
import { readBaseline } from './violations.mjs';
|
|
26
26
|
import { reportsDir, readJsonSafe } from './html-report.mjs';
|
|
27
27
|
import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
|
|
28
|
+
import { collectVsBaseFacts, discoverTeamBaseRef } from './team-parliament-io.mjs';
|
|
28
29
|
|
|
29
30
|
function sha256Hex(value) {
|
|
30
31
|
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
@@ -382,6 +383,17 @@ export function collectStatusFacts(options = {}) {
|
|
|
382
383
|
? 0
|
|
383
384
|
: null,
|
|
384
385
|
improvementCompass,
|
|
386
|
+
vsBase: (() => {
|
|
387
|
+
const vsRef = typeof options.vs === 'string' ? options.vs.trim() : '';
|
|
388
|
+
if (!vsRef) return null;
|
|
389
|
+
const baseRef = discoverTeamBaseRef(resolvedRoot, vsRef);
|
|
390
|
+
if (!baseRef) return null;
|
|
391
|
+
return collectVsBaseFacts({
|
|
392
|
+
root: resolvedRoot,
|
|
393
|
+
baseRef,
|
|
394
|
+
configRel: path.basename(configPath),
|
|
395
|
+
});
|
|
396
|
+
})(),
|
|
385
397
|
};
|
|
386
398
|
}
|
|
387
399
|
|
|
@@ -423,6 +435,7 @@ export function runStatusCommand(args = {}) {
|
|
|
423
435
|
expectedProjectId: args.expectedProjectId,
|
|
424
436
|
host: args.host,
|
|
425
437
|
arkgateVersion: args.arkgateVersion,
|
|
438
|
+
vs: args.vs,
|
|
426
439
|
});
|
|
427
440
|
|
|
428
441
|
if (asJson || args.json) {
|
|
@@ -471,6 +484,7 @@ export function runStatusCommand(args = {}) {
|
|
|
471
484
|
' · not a score'
|
|
472
485
|
);
|
|
473
486
|
}
|
|
487
|
+
if (manifest.vsBase?.line) write(` ${manifest.vsBase.line}`);
|
|
474
488
|
write(` next: [${manifest.nextAction.id}] ${manifest.nextAction.summary}`);
|
|
475
489
|
}
|
|
476
490
|
|
|
@@ -311,6 +311,9 @@ export function buildStatusManifest(facts) {
|
|
|
311
311
|
const compass = normalizeStatusImprovementCompass(facts.improvementCompass);
|
|
312
312
|
if (compass)
|
|
313
313
|
status.improvementCompass = compass;
|
|
314
|
+
if (facts.vsBase && typeof facts.vsBase.baseRef === 'string' && facts.vsBase.baseRef.length > 0) {
|
|
315
|
+
status.vsBase = facts.vsBase;
|
|
316
|
+
}
|
|
314
317
|
return status;
|
|
315
318
|
}
|
|
316
319
|
const STATUS_COMPASS_MODE_SET = new Set(STATUS_COMPASS_MODES);
|
|
@@ -576,5 +579,19 @@ export const ARK_STATUS_MANIFEST_SCHEMA = {
|
|
|
576
579
|
contractHash: { type: 'string', minLength: 1 },
|
|
577
580
|
},
|
|
578
581
|
},
|
|
582
|
+
vsBase: {
|
|
583
|
+
type: 'object',
|
|
584
|
+
description: 'Checkout vs a git base ref: pin, contract identity, baseline grow. Advisory honesty only — never a gate input.',
|
|
585
|
+
additionalProperties: false,
|
|
586
|
+
required: ['baseRef', 'line', 'pinLocal', 'pinBase', 'contractEqual', 'baselineGrew'],
|
|
587
|
+
properties: {
|
|
588
|
+
baseRef: { type: 'string', minLength: 1 },
|
|
589
|
+
line: { type: 'string', minLength: 1 },
|
|
590
|
+
pinLocal: { anyOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] },
|
|
591
|
+
pinBase: { anyOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] },
|
|
592
|
+
contractEqual: { type: 'boolean' },
|
|
593
|
+
baselineGrew: { type: 'boolean' },
|
|
594
|
+
},
|
|
595
|
+
},
|
|
579
596
|
},
|
|
580
597
|
};
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team parliament I/O — git base refs, changed paths, pin/contract/baseline compare.
|
|
3
|
+
* Pure classification lives in team-parliament.mjs (Domain).
|
|
4
|
+
*/
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import {
|
|
10
|
+
baselineKeysFromDocument,
|
|
11
|
+
classifyBaselineKeyDelta,
|
|
12
|
+
classifyChangeSet,
|
|
13
|
+
evaluateTeamGate,
|
|
14
|
+
formatVsBaseLine,
|
|
15
|
+
isTeamPersona,
|
|
16
|
+
mapPolicyClassToKind,
|
|
17
|
+
parseCodeownersHandles,
|
|
18
|
+
personaCheckBudget,
|
|
19
|
+
resolveStewardHandle,
|
|
20
|
+
suggestStewards,
|
|
21
|
+
} from './team-parliament.mjs';
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
baselineKeysFromDocument,
|
|
25
|
+
classifyBaselineKeyDelta,
|
|
26
|
+
classifyChangeSet,
|
|
27
|
+
evaluateTeamGate,
|
|
28
|
+
formatVsBaseLine,
|
|
29
|
+
isTeamPersona,
|
|
30
|
+
mapPolicyClassToKind,
|
|
31
|
+
parseCodeownersHandles,
|
|
32
|
+
personaCheckBudget,
|
|
33
|
+
resolveStewardHandle,
|
|
34
|
+
suggestStewards,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function runGit(cwd, args) {
|
|
38
|
+
return spawnSync('git', ['-C', cwd, ...args], {
|
|
39
|
+
encoding: 'utf8',
|
|
40
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function safeGitRef(value) {
|
|
45
|
+
return (
|
|
46
|
+
typeof value === 'string' &&
|
|
47
|
+
/^[A-Za-z0-9][A-Za-z0-9._/-]{0,200}$/.test(value) &&
|
|
48
|
+
!value.includes('..')
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function contractSessionFrom(args, env = process.env) {
|
|
53
|
+
if (args?.contractSession === true) return true;
|
|
54
|
+
const raw = env.ARK_CONTRACT_SESSION;
|
|
55
|
+
return raw === '1' || raw === 'true';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function resolveTeamAuthor(args, env = process.env) {
|
|
59
|
+
return resolveStewardHandle({
|
|
60
|
+
explicit: typeof args?.author === 'string' ? args.author : null,
|
|
61
|
+
githubActor: env.GITHUB_ACTOR,
|
|
62
|
+
arkSteward: env.ARK_STEWARD,
|
|
63
|
+
authorEmail: env.GIT_AUTHOR_EMAIL,
|
|
64
|
+
gitName: env.GIT_AUTHOR_NAME,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function discoverTeamBaseRef(root, preferred) {
|
|
69
|
+
if (safeGitRef(preferred)) return preferred;
|
|
70
|
+
const candidates = ['origin/dev', 'origin/main', 'origin/master', 'dev', 'main'];
|
|
71
|
+
for (const candidate of candidates) {
|
|
72
|
+
const exists = runGit(root, ['rev-parse', '--verify', `${candidate}^{commit}`]);
|
|
73
|
+
if (exists.status === 0) return candidate;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function listChangedPaths(root, baseRef) {
|
|
79
|
+
if (!safeGitRef(baseRef)) {
|
|
80
|
+
return { ok: false, paths: [], error: 'Invalid or missing git base ref.' };
|
|
81
|
+
}
|
|
82
|
+
const verify = runGit(root, ['rev-parse', '--verify', `${baseRef}^{commit}`]);
|
|
83
|
+
if (verify.status !== 0) {
|
|
84
|
+
return { ok: false, paths: [], error: `Cannot resolve git ref ${baseRef}.` };
|
|
85
|
+
}
|
|
86
|
+
const diff = runGit(root, ['diff', '--name-only', `${baseRef}...HEAD`]);
|
|
87
|
+
const unstaged = runGit(root, ['diff', '--name-only']);
|
|
88
|
+
const staged = runGit(root, ['diff', '--name-only', '--cached']);
|
|
89
|
+
const untracked = runGit(root, ['ls-files', '--others', '--exclude-standard']);
|
|
90
|
+
if (diff.status !== 0) {
|
|
91
|
+
return { ok: false, paths: [], error: diff.stderr.trim() || `git diff ${baseRef} failed.` };
|
|
92
|
+
}
|
|
93
|
+
const paths = [
|
|
94
|
+
...diff.stdout.split('\n'),
|
|
95
|
+
...unstaged.stdout.split('\n'),
|
|
96
|
+
...staged.stdout.split('\n'),
|
|
97
|
+
...untracked.stdout.split('\n'),
|
|
98
|
+
]
|
|
99
|
+
.map((line) => line.trim().replace(/\\/g, '/'))
|
|
100
|
+
.filter(Boolean);
|
|
101
|
+
return { ok: true, paths: [...new Set(paths)].sort(), error: null };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function gitShowText(root, baseRef, relPath) {
|
|
105
|
+
if (!safeGitRef(baseRef) || !relPath) return null;
|
|
106
|
+
const shown = runGit(root, ['show', `${baseRef}:${relPath}`]);
|
|
107
|
+
if (shown.status !== 0) return null;
|
|
108
|
+
return shown.stdout;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function readJsonMaybe(text) {
|
|
112
|
+
if (typeof text !== 'string' || !text.trim()) return null;
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(text);
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function arkgatePinFromPackageJson(raw) {
|
|
121
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
122
|
+
const bags = [raw.dependencies, raw.devDependencies, raw.optionalDependencies];
|
|
123
|
+
for (const bag of bags) {
|
|
124
|
+
if (bag && typeof bag.arkgate === 'string' && bag.arkgate.trim()) return bag.arkgate.trim();
|
|
125
|
+
}
|
|
126
|
+
if (raw.name === 'arkgate' && typeof raw.version === 'string') return raw.version;
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function sha256Text(text) {
|
|
131
|
+
return `sha256:${createHash('sha256').update(text, 'utf8').digest('hex')}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function collectVsBaseFacts({ root, baseRef, configRel = 'ark.config.json' }) {
|
|
135
|
+
const localConfigPath = path.join(root, configRel);
|
|
136
|
+
const localPkgPath = path.join(root, 'package.json');
|
|
137
|
+
const localBaselinePath = path.join(root, '.ark-baseline.json');
|
|
138
|
+
const localConfig = fs.existsSync(localConfigPath) ? fs.readFileSync(localConfigPath, 'utf8') : '';
|
|
139
|
+
const localPkg = fs.existsSync(localPkgPath)
|
|
140
|
+
? readJsonMaybe(fs.readFileSync(localPkgPath, 'utf8'))
|
|
141
|
+
: null;
|
|
142
|
+
const localBaseline = fs.existsSync(localBaselinePath)
|
|
143
|
+
? readJsonMaybe(fs.readFileSync(localBaselinePath, 'utf8'))
|
|
144
|
+
: null;
|
|
145
|
+
const baseConfig = gitShowText(root, baseRef, configRel) ?? '';
|
|
146
|
+
const basePkg = readJsonMaybe(gitShowText(root, baseRef, 'package.json'));
|
|
147
|
+
const baseBaseline = readJsonMaybe(gitShowText(root, baseRef, '.ark-baseline.json'));
|
|
148
|
+
const localKeys = baselineKeysFromDocument(localBaseline);
|
|
149
|
+
const baseKeys = baselineKeysFromDocument(baseBaseline);
|
|
150
|
+
const grew = classifyBaselineKeyDelta(baseKeys, localKeys).grow.length > 0;
|
|
151
|
+
const facts = {
|
|
152
|
+
baseRef,
|
|
153
|
+
pinLocal: arkgatePinFromPackageJson(localPkg),
|
|
154
|
+
pinBase: arkgatePinFromPackageJson(basePkg),
|
|
155
|
+
contractEqual: Boolean(localConfig) && localConfig === baseConfig,
|
|
156
|
+
baselineGrew: grew,
|
|
157
|
+
};
|
|
158
|
+
if (localConfig && baseConfig && !facts.contractEqual) {
|
|
159
|
+
facts.contractEqual = sha256Text(localConfig) === sha256Text(baseConfig);
|
|
160
|
+
}
|
|
161
|
+
return { ...facts, line: formatVsBaseLine(facts) };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function teamStewardsFromConfig(config) {
|
|
165
|
+
return Array.isArray(config?.stewards) ? config.stewards.filter((s) => typeof s === 'string') : [];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function applyPersonaDefaults(args) {
|
|
169
|
+
const next = { ...args };
|
|
170
|
+
if (!isTeamPersona(next.persona)) return next;
|
|
171
|
+
const budget = personaCheckBudget(next.persona);
|
|
172
|
+
if (budget.scan === 'none' || budget.scan === 'changed' || budget.scan === 'changed+ungoverned') {
|
|
173
|
+
next.changed = true;
|
|
174
|
+
}
|
|
175
|
+
if (budget.scan === 'changed+ungoverned') next.failUngoverned = true;
|
|
176
|
+
if (budget.contractDiff) next.contractDiff = true;
|
|
177
|
+
if (next.persona === 'steward') next.contractSession = true;
|
|
178
|
+
return next;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function bindTeamBaseRefs(args, root) {
|
|
182
|
+
const next = applyPersonaDefaults(args);
|
|
183
|
+
const teamBase = discoverTeamBaseRef(
|
|
184
|
+
root,
|
|
185
|
+
next.base || next.against || next.policyBaseRef || next.baseRef
|
|
186
|
+
);
|
|
187
|
+
if (teamBase) {
|
|
188
|
+
if (!next.against && (next.changed || next.contractDiff || next.persona)) {
|
|
189
|
+
next.against = teamBase;
|
|
190
|
+
}
|
|
191
|
+
if (!next.policyBaseRef && (next.contractDiff || next.changed || next.against)) {
|
|
192
|
+
next.policyBaseRef = teamBase;
|
|
193
|
+
}
|
|
194
|
+
if (!next.baseRef && next.failOnNewSmells) next.baseRef = teamBase;
|
|
195
|
+
}
|
|
196
|
+
return { args: next, teamBase };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function teamCheckRequested(args, config) {
|
|
200
|
+
return Boolean(
|
|
201
|
+
args.changed ||
|
|
202
|
+
args.contractDiff ||
|
|
203
|
+
args.against ||
|
|
204
|
+
args.persona ||
|
|
205
|
+
args.contractSession ||
|
|
206
|
+
(args.strictMerge && teamStewardsFromConfig(config).length > 0)
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function runTeamPreflight({ root, args, config, policyDelta, teamBase }) {
|
|
211
|
+
if (!teamCheckRequested(args, config)) {
|
|
212
|
+
return { halt: null, teamParliament: null, changedPaths: [] };
|
|
213
|
+
}
|
|
214
|
+
const againstRef = args.against || teamBase;
|
|
215
|
+
const listed = againstRef ? listChangedPaths(root, againstRef) : { ok: true, paths: [], error: null };
|
|
216
|
+
const changedPaths = listed.ok ? listed.paths : [];
|
|
217
|
+
const changeSet = classifyChangeSet(changedPaths);
|
|
218
|
+
const baseBaselineRaw = againstRef
|
|
219
|
+
? readJsonMaybe(gitShowText(root, againstRef, '.ark-baseline.json'))
|
|
220
|
+
: null;
|
|
221
|
+
const localBaselinePath = path.join(root, args.baseline || '.ark-baseline.json');
|
|
222
|
+
const localBaselineRaw = fs.existsSync(localBaselinePath)
|
|
223
|
+
? readJsonMaybe(fs.readFileSync(localBaselinePath, 'utf8'))
|
|
224
|
+
: null;
|
|
225
|
+
const baselineDelta = classifyBaselineKeyDelta(
|
|
226
|
+
baselineKeysFromDocument(baseBaselineRaw),
|
|
227
|
+
baselineKeysFromDocument(localBaselineRaw)
|
|
228
|
+
);
|
|
229
|
+
const policyKind = mapPolicyClassToKind(policyDelta?.classification ?? null);
|
|
230
|
+
const verdict = evaluateTeamGate({
|
|
231
|
+
changeSet,
|
|
232
|
+
contractSession: contractSessionFrom(args),
|
|
233
|
+
policyKind,
|
|
234
|
+
baselineGrowCount: baselineDelta.grow.length,
|
|
235
|
+
stewards: teamStewardsFromConfig(config),
|
|
236
|
+
author: resolveTeamAuthor(args),
|
|
237
|
+
});
|
|
238
|
+
const teamParliament = {
|
|
239
|
+
baseRef: againstRef,
|
|
240
|
+
changeSet,
|
|
241
|
+
policyKind,
|
|
242
|
+
baselineGrow: baselineDelta.grow.length,
|
|
243
|
+
baselineShrink: baselineDelta.shrink.length,
|
|
244
|
+
...verdict,
|
|
245
|
+
changedPathError: listed.ok ? null : listed.error,
|
|
246
|
+
};
|
|
247
|
+
if (!listed.ok && (args.changed || args.against || args.contractDiff)) {
|
|
248
|
+
return {
|
|
249
|
+
halt: { exitCode: 2, message: listed.error || 'Cannot resolve team base ref.', teamParliament },
|
|
250
|
+
teamParliament,
|
|
251
|
+
changedPaths,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (verdict.deny) {
|
|
255
|
+
return {
|
|
256
|
+
halt: { exitCode: 1, message: verdict.message, teamParliament, fail: true },
|
|
257
|
+
teamParliament,
|
|
258
|
+
changedPaths,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (args.changed && changeSet.productPaths.length === 0 && !changeSet.hasLaw) {
|
|
262
|
+
return {
|
|
263
|
+
halt: { exitCode: 0, cheap: true, teamParliament },
|
|
264
|
+
teamParliament,
|
|
265
|
+
changedPaths,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return { halt: null, teamParliament, changedPaths };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function ungovernedDumpMessage(dumped) {
|
|
272
|
+
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.`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function filterChangedGovernedFiles(allGovernedFiles, root, changedPaths, normalizeRel) {
|
|
276
|
+
if (!changedPaths?.length) return allGovernedFiles;
|
|
277
|
+
const changedSet = new Set(changedPaths);
|
|
278
|
+
return allGovernedFiles.filter((abs) => changedSet.has(normalizeRel(path.relative(root, abs))));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function applyAgainstRatchet({
|
|
282
|
+
violations,
|
|
283
|
+
againstRef,
|
|
284
|
+
root,
|
|
285
|
+
changed,
|
|
286
|
+
changedPaths,
|
|
287
|
+
occurrenceKeys,
|
|
288
|
+
}) {
|
|
289
|
+
const baseRaw = readJsonMaybe(gitShowText(root, againstRef, '.ark-baseline.json'));
|
|
290
|
+
const baseKeys = new Set(baselineKeysFromDocument(baseRaw));
|
|
291
|
+
const vsBaseActive = violations.filter((_, index) => !baseKeys.has(occurrenceKeys[index]));
|
|
292
|
+
const changedSet = new Set(changedPaths ?? []);
|
|
293
|
+
const activeViolations = changed
|
|
294
|
+
? vsBaseActive.filter((violation) =>
|
|
295
|
+
changedSet.has(String(violation.file || '').replace(/\\/g, '/'))
|
|
296
|
+
)
|
|
297
|
+
: vsBaseActive;
|
|
298
|
+
return {
|
|
299
|
+
activeViolations,
|
|
300
|
+
suppressed: violations.filter((violation) => !activeViolations.includes(violation)),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function gitAuthors(root) {
|
|
305
|
+
const log = runGit(root, ['log', '--format=%aN<%aE>', '--max-count=300']);
|
|
306
|
+
if (log.status !== 0) return [];
|
|
307
|
+
const ids = [];
|
|
308
|
+
for (const line of log.stdout.split('\n')) {
|
|
309
|
+
const match = line.trim().match(/^(.*)<([^>]+)>$/);
|
|
310
|
+
if (!match) continue;
|
|
311
|
+
const name = match[1].trim();
|
|
312
|
+
const email = match[2].trim();
|
|
313
|
+
ids.push(email || name);
|
|
314
|
+
}
|
|
315
|
+
return ids;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function readCodeowners(root) {
|
|
319
|
+
for (const rel of ['CODEOWNERS', '.github/CODEOWNERS', 'docs/CODEOWNERS']) {
|
|
320
|
+
const full = path.join(root, rel);
|
|
321
|
+
if (!fs.existsSync(full)) continue;
|
|
322
|
+
try {
|
|
323
|
+
return parseCodeownersHandles(fs.readFileSync(full, 'utf8'));
|
|
324
|
+
} catch {
|
|
325
|
+
return [];
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Advisory only. Never flips a gate. Missing git is honest empty, not green. */
|
|
332
|
+
export function collectStewardNudge(root, config) {
|
|
333
|
+
return suggestStewards({
|
|
334
|
+
existingStewards: teamStewardsFromConfig(config),
|
|
335
|
+
gitAuthors: gitAuthors(root),
|
|
336
|
+
codeowners: readCodeowners(root),
|
|
337
|
+
});
|
|
338
|
+
}
|