arkgate 4.3.0 → 4.5.0
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 +81 -10
- package/README.md +11 -7
- package/bin/ark-check-runtime.mjs +50 -14
- package/bin/ark-layer-match.mjs +25 -12
- package/bin/lib/agent-projection.mjs +1 -1
- package/bin/lib/analysis-engine.mjs +5 -5
- package/bin/lib/ci-and-commands.mjs +11 -5
- package/bin/lib/doctor-plan.mjs +25 -0
- package/bin/lib/html-report-advisories.mjs +33 -0
- package/bin/lib/html-report-depth.mjs +24 -0
- package/bin/lib/html-report.mjs +8 -1
- package/bin/lib/improvement-compass-doctor.mjs +106 -0
- package/bin/lib/improvement-compass-map.mjs +507 -0
- package/bin/lib/improvement-compass-types.mjs +85 -0
- package/bin/lib/improvement-compass.mjs +79 -0
- package/bin/lib/managed-upgrade-honesty.mjs +201 -0
- package/bin/lib/managed-upgrade.mjs +37 -4
- package/bin/lib/status-command.mjs +127 -2
- package/bin/lib/status-manifest.mjs +187 -1
- package/dist/eslint/index.cjs +2 -2
- package/dist/eslint/index.js +2 -2
- package/dist/index.cjs +35 -35
- package/dist/index.d.ts +267 -5
- package/dist/index.js +35 -35
- package/docs/README.md +7 -6
- package/docs/agent-guide.md +50 -7
- package/docs/develop.md +9 -4
- package/docs/diagnostics.md +1 -1
- package/docs/package-surface.md +38 -34
- package/docs/product-voice.md +30 -1
- package/docs/use.md +67 -1
- package/package.json +1 -1
- package/schemas/ark.status-manifest.schema.json +53 -0
- package/server.json +2 -2
- package/templates/agent-skills/README.md +2 -2
- package/templates/agent-skills/ark-adopt/SKILL.md +20 -0
- package/templates/agent-skills/ark-architect/SKILL.md +21 -1
- package/templates/agent-skills/ark-autopilot/SKILL.md +25 -5
- package/templates/agent-skills/ark-contract/SKILL.md +20 -0
- package/templates/agent-skills/ark-coverage/SKILL.md +20 -0
- package/templates/agent-skills/ark-explain/SKILL.md +20 -0
- package/templates/agent-skills/ark-explore/SKILL.md +23 -3
- package/templates/agent-skills/ark-fix/SKILL.md +22 -2
- package/templates/agent-skills/ark-loop/SKILL.md +22 -2
- package/templates/agent-skills/ark-place/SKILL.md +20 -0
- package/templates/agent-skills/ark-runtime/SKILL.md +7 -0
- package/templates/agent-skills/ark-think/SKILL.md +20 -0
- package/templates/agent-skills/ark-upgrade/SKILL.md +20 -0
- package/templates/skills/ark-adopt.md +20 -0
- package/templates/skills/ark-architect.md +21 -1
- package/templates/skills/ark-autopilot.md +25 -5
- package/templates/skills/ark-contract.md +20 -0
- package/templates/skills/ark-coverage.md +20 -0
- package/templates/skills/ark-explain.md +20 -0
- package/templates/skills/ark-explore.md +23 -3
- package/templates/skills/ark-fix.md +22 -2
- package/templates/skills/ark-loop.md +22 -2
- package/templates/skills/ark-place.md +20 -0
- package/templates/skills/ark-runtime.md +7 -0
- package/templates/skills/ark-think.md +20 -0
- package/templates/skills/ark-upgrade.md +20 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DF05 — managed-upgrade self-service honesty (one residual pilot).
|
|
3
|
+
*
|
|
4
|
+
* Self-service criterion (must stay answerable from package surfaces without a maintainer):
|
|
5
|
+
* After a managed upgrade (or equivalent), can a consumer learn from package surfaces whether
|
|
6
|
+
* the write-path is still honestly labeled active/advisory and whether customized install
|
|
7
|
+
* content was preserved — without asking a maintainer?
|
|
8
|
+
*
|
|
9
|
+
* This module projects that answer onto `ark upgrade` JSON/human output:
|
|
10
|
+
* - write-path activation labels per selected host (hard | advisory | unavailable)
|
|
11
|
+
* - customized (and conflicted) content-identity preserve proof
|
|
12
|
+
*
|
|
13
|
+
* Soft hosts never claim hard. Upgrade never invents hardWriteActive from disk alone —
|
|
14
|
+
* hard requires runtime evidence elsewhere (hooks/doctor/status); upgrade labels fail-closed.
|
|
15
|
+
* Always notAScore; never a gate input.
|
|
16
|
+
*/
|
|
17
|
+
import { getHostSupportProfile } from './host-support-matrix.mjs';
|
|
18
|
+
import { classifyStatusWritePath, defaultHonestLabel } from './status-manifest.mjs';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} host
|
|
22
|
+
* @param {{ hardWriteActive?: boolean }} [evidence]
|
|
23
|
+
* @returns {{
|
|
24
|
+
* host: string,
|
|
25
|
+
* writePath: 'hard'|'advisory'|'unavailable',
|
|
26
|
+
* softWriteHost: boolean,
|
|
27
|
+
* hardWriteSupported: boolean,
|
|
28
|
+
* hardWriteActive: boolean,
|
|
29
|
+
* label: string,
|
|
30
|
+
* }}
|
|
31
|
+
*/
|
|
32
|
+
export function projectHostWritePathActivation(host, evidence = {}) {
|
|
33
|
+
const normalized = typeof host === 'string' ? host.trim().toLowerCase() : '';
|
|
34
|
+
if (!normalized) {
|
|
35
|
+
return {
|
|
36
|
+
host: 'unknown',
|
|
37
|
+
writePath: 'unavailable',
|
|
38
|
+
softWriteHost: false,
|
|
39
|
+
hardWriteSupported: false,
|
|
40
|
+
hardWriteActive: false,
|
|
41
|
+
label: defaultHonestLabel('unavailable', null),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const profile = getHostSupportProfile(normalized);
|
|
46
|
+
if (!profile) {
|
|
47
|
+
return {
|
|
48
|
+
host: normalized,
|
|
49
|
+
writePath: 'unavailable',
|
|
50
|
+
softWriteHost: false,
|
|
51
|
+
hardWriteSupported: false,
|
|
52
|
+
hardWriteActive: false,
|
|
53
|
+
label: defaultHonestLabel('unavailable', normalized),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const hardWriteSupported = profile.capabilities?.['hard-write'] === true;
|
|
58
|
+
const softWriteHost = !hardWriteSupported;
|
|
59
|
+
// Fail-closed: soft never hard; hard only when caller supplies proven active evidence.
|
|
60
|
+
const hardWriteActive =
|
|
61
|
+
!softWriteHost && hardWriteSupported && evidence.hardWriteActive === true;
|
|
62
|
+
const writePath = classifyStatusWritePath({
|
|
63
|
+
softWriteHost,
|
|
64
|
+
hardWriteActive,
|
|
65
|
+
activeHost: normalized,
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
host: normalized,
|
|
69
|
+
writePath,
|
|
70
|
+
softWriteHost,
|
|
71
|
+
hardWriteSupported,
|
|
72
|
+
hardWriteActive,
|
|
73
|
+
label: defaultHonestLabel(writePath, normalized),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Project self-service honesty facts from a managed upgrade plan.
|
|
79
|
+
*
|
|
80
|
+
* @param {{
|
|
81
|
+
* hosts?: string[],
|
|
82
|
+
* assets?: Array<{ path?: string, state?: string, willApply?: boolean, blocked?: boolean }>,
|
|
83
|
+
* summary?: { customizedPreserved?: number, blocked?: number, states?: Record<string, number> },
|
|
84
|
+
* }} plan
|
|
85
|
+
* @param {{
|
|
86
|
+
* hardWriteActiveByHost?: Record<string, boolean>,
|
|
87
|
+
* }} [options]
|
|
88
|
+
* @returns {{
|
|
89
|
+
* schemaVersion: '1.0',
|
|
90
|
+
* notAScore: true,
|
|
91
|
+
* criterionId: 'df05-upgrade-activation-preserve',
|
|
92
|
+
* customizedPreserved: number,
|
|
93
|
+
* customizedPaths: string[],
|
|
94
|
+
* conflictedPaths: string[],
|
|
95
|
+
* customizedContentPreserved: boolean,
|
|
96
|
+
* writePathActivation: ReturnType<typeof projectHostWritePathActivation>[],
|
|
97
|
+
* writePathHonestlyLabeled: boolean,
|
|
98
|
+
* answers: {
|
|
99
|
+
* writePathActivationLabeled: boolean,
|
|
100
|
+
* customizedContentPreserved: boolean,
|
|
101
|
+
* },
|
|
102
|
+
* }}
|
|
103
|
+
*/
|
|
104
|
+
export function projectManagedUpgradeSelfServiceHonesty(plan, options = {}) {
|
|
105
|
+
const assets = Array.isArray(plan?.assets) ? plan.assets : [];
|
|
106
|
+
const hosts = Array.isArray(plan?.hosts) ? plan.hosts : [];
|
|
107
|
+
const hardByHost =
|
|
108
|
+
options.hardWriteActiveByHost && typeof options.hardWriteActiveByHost === 'object'
|
|
109
|
+
? options.hardWriteActiveByHost
|
|
110
|
+
: {};
|
|
111
|
+
|
|
112
|
+
const customizedPaths = assets
|
|
113
|
+
.filter((asset) => asset?.state === 'customized' && typeof asset.path === 'string')
|
|
114
|
+
.map((asset) => asset.path)
|
|
115
|
+
.sort();
|
|
116
|
+
const conflictedPaths = assets
|
|
117
|
+
.filter((asset) => asset?.state === 'conflicted' && typeof asset.path === 'string')
|
|
118
|
+
.map((asset) => asset.path)
|
|
119
|
+
.sort();
|
|
120
|
+
|
|
121
|
+
// Preserve contract: customized assets must never be scheduled writes without consent.
|
|
122
|
+
const customizedContentPreserved = assets
|
|
123
|
+
.filter((asset) => asset?.state === 'customized' || asset?.state === 'conflicted')
|
|
124
|
+
.every((asset) => asset.willApply !== true);
|
|
125
|
+
|
|
126
|
+
const summaryCount =
|
|
127
|
+
typeof plan?.summary?.customizedPreserved === 'number'
|
|
128
|
+
? plan.summary.customizedPreserved
|
|
129
|
+
: customizedPaths.length;
|
|
130
|
+
|
|
131
|
+
const writePathActivation = hosts.map((host) => {
|
|
132
|
+
const key = typeof host === 'string' ? host.trim().toLowerCase() : '';
|
|
133
|
+
return projectHostWritePathActivation(host, {
|
|
134
|
+
hardWriteActive: hardByHost[key] === true,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Soft hosts never labeled hard; hard only when evidence supplied.
|
|
139
|
+
const writePathHonestlyLabeled = writePathActivation.every((entry) => {
|
|
140
|
+
if (entry.softWriteHost && entry.writePath === 'hard') return false;
|
|
141
|
+
if (entry.softWriteHost && entry.hardWriteActive) return false;
|
|
142
|
+
if (!entry.hardWriteSupported && entry.writePath === 'hard') return false;
|
|
143
|
+
if (entry.writePath === 'hard' && !entry.hardWriteActive) return false;
|
|
144
|
+
return true;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const answers = {
|
|
148
|
+
// Empty host list is not "labeled activation" — only claim labeled when hosts were projected.
|
|
149
|
+
writePathActivationLabeled:
|
|
150
|
+
writePathActivation.length > 0 && writePathHonestlyLabeled,
|
|
151
|
+
customizedContentPreserved,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
schemaVersion: '1.0',
|
|
156
|
+
notAScore: true,
|
|
157
|
+
criterionId: 'df05-upgrade-activation-preserve',
|
|
158
|
+
customizedPreserved: summaryCount,
|
|
159
|
+
customizedPaths,
|
|
160
|
+
conflictedPaths,
|
|
161
|
+
customizedContentPreserved,
|
|
162
|
+
writePathActivation,
|
|
163
|
+
writePathHonestlyLabeled,
|
|
164
|
+
answers,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Human one-liner block for upgrade preview/apply (stdout).
|
|
170
|
+
* @param {ReturnType<typeof projectManagedUpgradeSelfServiceHonesty>} honesty
|
|
171
|
+
*/
|
|
172
|
+
export function formatManagedUpgradeSelfServiceHonesty(honesty) {
|
|
173
|
+
if (!honesty) return [];
|
|
174
|
+
const lines = ['Self-service honesty (no maintainer required):'];
|
|
175
|
+
if (honesty.writePathActivation.length === 0) {
|
|
176
|
+
lines.push(' Write-path: shared/gates only (no host selected) — activation unavailable.');
|
|
177
|
+
} else {
|
|
178
|
+
for (const entry of honesty.writePathActivation) {
|
|
179
|
+
const soft = entry.softWriteHost ? 'soft host' : 'hard-capable';
|
|
180
|
+
const active = entry.hardWriteActive ? 'active' : 'not proven this invocation';
|
|
181
|
+
lines.push(
|
|
182
|
+
` Write-path ${entry.host}: ${entry.writePath} (${soft}; hard ${active}).`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (honesty.customizedPaths.length > 0) {
|
|
187
|
+
lines.push(
|
|
188
|
+
` Customized preserved: ${honesty.customizedPreserved} (${honesty.customizedPaths.join(', ')}).`
|
|
189
|
+
);
|
|
190
|
+
} else {
|
|
191
|
+
lines.push(
|
|
192
|
+
` Customized preserved: ${honesty.customizedPreserved} (no customized managed assets).`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (honesty.conflictedPaths.length > 0) {
|
|
196
|
+
lines.push(
|
|
197
|
+
` Conflicted (consent required): ${honesty.conflictedPaths.join(', ')}.`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return lines;
|
|
201
|
+
}
|
|
@@ -4,6 +4,10 @@ import path from 'node:path';
|
|
|
4
4
|
|
|
5
5
|
import { codexPrimaryTable, upsertCodexMcpTable } from './codex-home.mjs';
|
|
6
6
|
import { buildManagedAssetCatalog } from './install-migrate.mjs';
|
|
7
|
+
import {
|
|
8
|
+
formatManagedUpgradeSelfServiceHonesty,
|
|
9
|
+
projectManagedUpgradeSelfServiceHonesty,
|
|
10
|
+
} from './managed-upgrade-honesty.mjs';
|
|
7
11
|
import {
|
|
8
12
|
KNOWN_TOOLS,
|
|
9
13
|
arkPackageVersion,
|
|
@@ -12,6 +16,12 @@ import {
|
|
|
12
16
|
skillContentIdentity,
|
|
13
17
|
} from './skill-install.mjs';
|
|
14
18
|
|
|
19
|
+
export {
|
|
20
|
+
formatManagedUpgradeSelfServiceHonesty,
|
|
21
|
+
projectHostWritePathActivation,
|
|
22
|
+
projectManagedUpgradeSelfServiceHonesty,
|
|
23
|
+
} from './managed-upgrade-honesty.mjs';
|
|
24
|
+
|
|
15
25
|
export const MANAGED_MANIFEST_PATH = 'ark.managed.json';
|
|
16
26
|
const MANIFEST_VERSION = '1.0';
|
|
17
27
|
const AFTER_CONTENT = Symbol('managed-after-content');
|
|
@@ -534,7 +544,10 @@ export function planManagedUpgrade(root, options = {}) {
|
|
|
534
544
|
}
|
|
535
545
|
|
|
536
546
|
function publicPlan(plan, overrides = {}) {
|
|
537
|
-
|
|
547
|
+
const assets = plan.assets.map(
|
|
548
|
+
({ containerBeforeHash: _container, [AFTER_CONTENT]: _content, ...asset }) => asset
|
|
549
|
+
);
|
|
550
|
+
const base = {
|
|
538
551
|
schemaVersion: plan.schemaVersion,
|
|
539
552
|
root: plan.root,
|
|
540
553
|
readOnly: overrides.readOnly ?? plan.readOnly,
|
|
@@ -544,11 +557,21 @@ function publicPlan(plan, overrides = {}) {
|
|
|
544
557
|
profile: plan.profile,
|
|
545
558
|
hosts: plan.hosts,
|
|
546
559
|
acceptConflicts: plan.acceptConflicts,
|
|
547
|
-
assets
|
|
548
|
-
({ containerBeforeHash: _container, [AFTER_CONTENT]: _content, ...asset }) => asset
|
|
549
|
-
),
|
|
560
|
+
assets,
|
|
550
561
|
summary: plan.summary,
|
|
562
|
+
};
|
|
563
|
+
// DF05: self-service honesty (write-path labels + customized preserve) on every public plan.
|
|
564
|
+
// Not part of planDigest — advisory projection only; never invents hard write on soft hosts.
|
|
565
|
+
const selfService = projectManagedUpgradeSelfServiceHonesty({
|
|
566
|
+
hosts: base.hosts,
|
|
567
|
+
assets,
|
|
568
|
+
summary: base.summary,
|
|
569
|
+
});
|
|
570
|
+
return {
|
|
571
|
+
...base,
|
|
551
572
|
...overrides,
|
|
573
|
+
// DF05 projection always present unless an override supplies a replacement.
|
|
574
|
+
selfService: overrides.selfService ?? selfService,
|
|
552
575
|
};
|
|
553
576
|
}
|
|
554
577
|
|
|
@@ -738,6 +761,16 @@ export function renderManagedUpgrade(plan, options = {}) {
|
|
|
738
761
|
`Managed assets: ${managedAssets}; would write: ${wouldWrite}; ` +
|
|
739
762
|
`customized preserved: ${customizedPreserved}; blocked conflicts/deletions: ${blocked}.`
|
|
740
763
|
);
|
|
764
|
+
const honesty =
|
|
765
|
+
plan.selfService ??
|
|
766
|
+
projectManagedUpgradeSelfServiceHonesty({
|
|
767
|
+
hosts: plan.hosts,
|
|
768
|
+
assets: plan.assets,
|
|
769
|
+
summary: plan.summary,
|
|
770
|
+
});
|
|
771
|
+
for (const line of formatManagedUpgradeSelfServiceHonesty(honesty)) {
|
|
772
|
+
console.log(line);
|
|
773
|
+
}
|
|
741
774
|
if (plan.applied) {
|
|
742
775
|
console.log(
|
|
743
776
|
`Applied ${wouldWrite} content write(s)` +
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ACS03 — gather session/project evidence for `ark status` / MCP `ark_status`.
|
|
2
|
+
* ACS03 + DF02 — gather session/project evidence for `ark status` / MCP `ark_status`.
|
|
3
3
|
*
|
|
4
4
|
* Fail-closed and CI-safe: never prompts (no readline), never invents hard write,
|
|
5
5
|
* never invents a numeric score. Pure assembly lives in Domain statusManifest.
|
|
6
|
+
* Improvement compass carries explicit honesty mode (full|subset|unavailable).
|
|
6
7
|
*/
|
|
7
8
|
import { createHash } from 'node:crypto';
|
|
8
9
|
import fs from 'node:fs';
|
|
9
10
|
import path from 'node:path';
|
|
10
11
|
import { fileURLToPath } from 'node:url';
|
|
11
12
|
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
buildStatusManifest,
|
|
15
|
+
normalizeStatusImprovementCompass,
|
|
16
|
+
projectStatusImprovementCompass,
|
|
17
|
+
unavailableStatusImprovementCompass,
|
|
18
|
+
} from './status-manifest.mjs';
|
|
13
19
|
import { createProjectId } from './project-identity.mjs';
|
|
14
20
|
import { resolveEffectiveProjectRoot } from './project-root.mjs';
|
|
15
21
|
import { detectWritePathCapabilities } from './write-path-detect.mjs';
|
|
@@ -130,6 +136,94 @@ export function lastCheckFactsFromSnapshot(latest, baseline) {
|
|
|
130
136
|
};
|
|
131
137
|
}
|
|
132
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Project status improvementCompass honesty from a report/session snapshot (DF02).
|
|
141
|
+
* Prefer stored thin slice; never invent green residual when facts are missing.
|
|
142
|
+
*
|
|
143
|
+
* @param {object|null|undefined} latest
|
|
144
|
+
* @param {{ contractHash?: string|null }} [opts]
|
|
145
|
+
* @returns {import('./status-manifest.mjs').StatusImprovementCompassSlice}
|
|
146
|
+
*/
|
|
147
|
+
export function statusCompassFromSnapshot(latest, opts = {}) {
|
|
148
|
+
const contractHash =
|
|
149
|
+
typeof opts.contractHash === 'string' && opts.contractHash.length > 0
|
|
150
|
+
? opts.contractHash
|
|
151
|
+
: null;
|
|
152
|
+
|
|
153
|
+
if (!latest || typeof latest !== 'object') {
|
|
154
|
+
return unavailableStatusImprovementCompass({
|
|
155
|
+
reasonCode: 'NO_SESSION_SNAPSHOT',
|
|
156
|
+
reason:
|
|
157
|
+
'No session report snapshot yet — run ark-check --doctor or --report for residual lenses. Status never invents green.',
|
|
158
|
+
contractHash,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Prefer explicit thin status slice on the snapshot (report path stores mode+residual).
|
|
163
|
+
if (latest.improvementCompass && typeof latest.improvementCompass === 'object') {
|
|
164
|
+
const normalized = normalizeStatusImprovementCompass({
|
|
165
|
+
...latest.improvementCompass,
|
|
166
|
+
...(contractHash && !latest.improvementCompass.contractHash
|
|
167
|
+
? { contractHash }
|
|
168
|
+
: {}),
|
|
169
|
+
factsSource: latest.improvementCompass.factsSource || 'report-snapshot',
|
|
170
|
+
});
|
|
171
|
+
if (normalized) return normalized;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Doctor-equivalent residual ids stored without honesty wrapper → full if complete flag set.
|
|
175
|
+
if (
|
|
176
|
+
latest.doctorImprovementCompass &&
|
|
177
|
+
typeof latest.doctorImprovementCompass === 'object' &&
|
|
178
|
+
latest.doctorImprovementCompass.notAScore === true &&
|
|
179
|
+
Array.isArray(latest.doctorImprovementCompass.topResidual)
|
|
180
|
+
) {
|
|
181
|
+
const complete = latest.compassFactsComplete === true || latest.completeness === 'complete';
|
|
182
|
+
return projectStatusImprovementCompass({
|
|
183
|
+
mode: complete ? 'full' : 'subset',
|
|
184
|
+
topResidual: latest.doctorImprovementCompass.topResidual,
|
|
185
|
+
reasonCode: complete ? undefined : 'FACTS_PARTIAL',
|
|
186
|
+
reason: complete
|
|
187
|
+
? undefined
|
|
188
|
+
: 'Session snapshot residual is partial — re-run doctor/report for full compass.',
|
|
189
|
+
factsSource: 'report-snapshot',
|
|
190
|
+
contractHash,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return unavailableStatusImprovementCompass({
|
|
195
|
+
reasonCode: 'NO_SESSION_SNAPSHOT',
|
|
196
|
+
reason:
|
|
197
|
+
'Session snapshot has no improvement compass facts — run ark-check --doctor or --report. Status never invents green.',
|
|
198
|
+
contractHash,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Build a storeable thin status compass from a full doctor ImprovementCompass (DF02).
|
|
204
|
+
* Used by report snapshot so status residual ⊆ doctor residual for the same tree.
|
|
205
|
+
*
|
|
206
|
+
* @param {{ notAScore?: boolean, topResidual?: string[] }|null|undefined} doctorCompass
|
|
207
|
+
* @param {{ mode?: 'full'|'subset', contractHash?: string|null, reasonCode?: string, reason?: string }} [opts]
|
|
208
|
+
*/
|
|
209
|
+
export function thinStatusCompassFromDoctor(doctorCompass, opts = {}) {
|
|
210
|
+
if (!doctorCompass || doctorCompass.notAScore !== true) {
|
|
211
|
+
return unavailableStatusImprovementCompass({
|
|
212
|
+
reasonCode: 'FACTS_UNAVAILABLE',
|
|
213
|
+
contractHash: opts.contractHash,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const mode = opts.mode === 'subset' ? 'subset' : 'full';
|
|
217
|
+
return projectStatusImprovementCompass({
|
|
218
|
+
mode,
|
|
219
|
+
topResidual: Array.isArray(doctorCompass.topResidual) ? doctorCompass.topResidual : [],
|
|
220
|
+
reasonCode: opts.reasonCode,
|
|
221
|
+
reason: opts.reason,
|
|
222
|
+
factsSource: 'report-snapshot',
|
|
223
|
+
contractHash: opts.contractHash,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
133
227
|
/**
|
|
134
228
|
* Collect status facts from disk (no prompts).
|
|
135
229
|
* @param {{
|
|
@@ -140,6 +234,8 @@ export function lastCheckFactsFromSnapshot(latest, baseline) {
|
|
|
140
234
|
* host?: string,
|
|
141
235
|
* arkgateVersion?: string,
|
|
142
236
|
* env?: NodeJS.ProcessEnv,
|
|
237
|
+
* improvementCompass?: object | null,
|
|
238
|
+
* contractHash?: string | null,
|
|
143
239
|
* }} [options]
|
|
144
240
|
*/
|
|
145
241
|
export function collectStatusFacts(options = {}) {
|
|
@@ -242,6 +338,22 @@ export function collectStatusFacts(options = {}) {
|
|
|
242
338
|
|
|
243
339
|
const arkruleFrozenFallback = countArkruleFrozenKeys(baseline);
|
|
244
340
|
|
|
341
|
+
// DF02 — always project compass with honesty mode (never invent green residual).
|
|
342
|
+
// Prefer explicit override (tests/MCP inject doctor-facts); else report snapshot.
|
|
343
|
+
let improvementCompass = null;
|
|
344
|
+
if (options.improvementCompass != null) {
|
|
345
|
+
improvementCompass = normalizeStatusImprovementCompass(options.improvementCompass);
|
|
346
|
+
}
|
|
347
|
+
if (!improvementCompass) {
|
|
348
|
+
const contractHash =
|
|
349
|
+
typeof options.contractHash === 'string' && options.contractHash.length > 0
|
|
350
|
+
? options.contractHash
|
|
351
|
+
: configExists && config
|
|
352
|
+
? sha256Hex(JSON.stringify(config))
|
|
353
|
+
: null;
|
|
354
|
+
improvementCompass = statusCompassFromSnapshot(latest, { contractHash });
|
|
355
|
+
}
|
|
356
|
+
|
|
245
357
|
return {
|
|
246
358
|
arkgateVersion: options.arkgateVersion || packageVersion(),
|
|
247
359
|
resolvedRoot: realpathOrResolve(resolvedRoot),
|
|
@@ -269,6 +381,7 @@ export function collectStatusFacts(options = {}) {
|
|
|
269
381
|
: arkRulesLoaded
|
|
270
382
|
? 0
|
|
271
383
|
: null,
|
|
384
|
+
improvementCompass,
|
|
272
385
|
};
|
|
273
386
|
}
|
|
274
387
|
|
|
@@ -346,6 +459,18 @@ export function runStatusCommand(args = {}) {
|
|
|
346
459
|
? ` · frozen=${manifest.rules.frozenResidual}`
|
|
347
460
|
: '')
|
|
348
461
|
);
|
|
462
|
+
const ic = manifest.improvementCompass;
|
|
463
|
+
if (ic) {
|
|
464
|
+
const residual =
|
|
465
|
+
Array.isArray(ic.topResidual) && ic.topResidual.length > 0
|
|
466
|
+
? ic.topResidual.join(', ')
|
|
467
|
+
: '(none)';
|
|
468
|
+
write(
|
|
469
|
+
` compass: mode=${ic.mode}` +
|
|
470
|
+
(ic.mode === 'unavailable' ? '' : ` · residual=${residual}`) +
|
|
471
|
+
' · not a score'
|
|
472
|
+
);
|
|
473
|
+
}
|
|
349
474
|
write(` next: [${manifest.nextAction.id}] ${manifest.nextAction.summary}`);
|
|
350
475
|
}
|
|
351
476
|
|
|
@@ -10,6 +10,25 @@
|
|
|
10
10
|
|
|
11
11
|
export const ARK_STATUS_MANIFEST_SCHEMA_VERSION = '1.0';
|
|
12
12
|
export const ARK_STATUS_MANIFEST_SCHEMA_URL = 'https://unpkg.com/arkgate@4/schemas/ark.status-manifest.schema.json';
|
|
13
|
+
/**
|
|
14
|
+
* Honesty mode for status improvementCompass (DF02).
|
|
15
|
+
* - full: residual projected from doctor-equivalent facts (residual ⊆ doctor)
|
|
16
|
+
* - subset: incomplete facts; residual may omit doctor residual; never invent green
|
|
17
|
+
* - unavailable: no usable facts; empty residual + reason (never silent ok)
|
|
18
|
+
*/
|
|
19
|
+
export const STATUS_COMPASS_MODES = ['full', 'subset', 'unavailable'];
|
|
20
|
+
/** Provenance for status compass residual (same-tree intent). */
|
|
21
|
+
export const STATUS_COMPASS_FACTS_SOURCES = [
|
|
22
|
+
'doctor-facts',
|
|
23
|
+
'report-snapshot',
|
|
24
|
+
'none',
|
|
25
|
+
];
|
|
26
|
+
/** Stable reason codes when mode is not full. */
|
|
27
|
+
export const STATUS_COMPASS_REASON_CODES = {
|
|
28
|
+
FACTS_UNAVAILABLE: 'FACTS_UNAVAILABLE',
|
|
29
|
+
FACTS_PARTIAL: 'FACTS_PARTIAL',
|
|
30
|
+
NO_SESSION_SNAPSHOT: 'NO_SESSION_SNAPSHOT',
|
|
31
|
+
};
|
|
13
32
|
const PROJECT_ID_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
14
33
|
/**
|
|
15
34
|
* Evaluate project binding for status without filesystem.
|
|
@@ -278,7 +297,7 @@ export function buildStatusManifest(facts) {
|
|
|
278
297
|
...(binding.code ? { code: binding.code } : {}),
|
|
279
298
|
...(binding.message ? { message: binding.message } : {}),
|
|
280
299
|
};
|
|
281
|
-
|
|
300
|
+
const status = {
|
|
282
301
|
schemaVersion: ARK_STATUS_MANIFEST_SCHEMA_VERSION,
|
|
283
302
|
arkgateVersion: typeof facts.arkgateVersion === 'string' && facts.arkgateVersion.length > 0
|
|
284
303
|
? facts.arkgateVersion
|
|
@@ -289,6 +308,153 @@ export function buildStatusManifest(facts) {
|
|
|
289
308
|
rules,
|
|
290
309
|
nextAction: resolveStatusNextAction(facts, binding, activation, lastCheck, rules),
|
|
291
310
|
};
|
|
311
|
+
const compass = normalizeStatusImprovementCompass(facts.improvementCompass);
|
|
312
|
+
if (compass)
|
|
313
|
+
status.improvementCompass = compass;
|
|
314
|
+
return status;
|
|
315
|
+
}
|
|
316
|
+
const STATUS_COMPASS_MODE_SET = new Set(STATUS_COMPASS_MODES);
|
|
317
|
+
const STATUS_COMPASS_SOURCE_SET = new Set(STATUS_COMPASS_FACTS_SOURCES);
|
|
318
|
+
/**
|
|
319
|
+
* Project a thin status improvementCompass with explicit honesty mode (DF02).
|
|
320
|
+
*
|
|
321
|
+
* Rules:
|
|
322
|
+
* - always notAScore: true
|
|
323
|
+
* - mode full | subset | unavailable (invalid mode → unavailable)
|
|
324
|
+
* - unavailable: topResidual forced empty (never invent residual or silent green)
|
|
325
|
+
* - full/subset: residual ids from input only (never fabricate ok lenses)
|
|
326
|
+
* - never carries valid / goal.met / score fields
|
|
327
|
+
*/
|
|
328
|
+
export function projectStatusImprovementCompass(input) {
|
|
329
|
+
const mode = STATUS_COMPASS_MODE_SET.has(input.mode)
|
|
330
|
+
? input.mode
|
|
331
|
+
: 'unavailable';
|
|
332
|
+
const factsSource = input.factsSource != null && STATUS_COMPASS_SOURCE_SET.has(input.factsSource)
|
|
333
|
+
? input.factsSource
|
|
334
|
+
: mode === 'unavailable'
|
|
335
|
+
? 'none'
|
|
336
|
+
: undefined;
|
|
337
|
+
const contractHash = typeof input.contractHash === 'string' && input.contractHash.length > 0
|
|
338
|
+
? input.contractHash
|
|
339
|
+
: undefined;
|
|
340
|
+
if (mode === 'unavailable') {
|
|
341
|
+
const out = {
|
|
342
|
+
schemaVersion: '1.0',
|
|
343
|
+
notAScore: true,
|
|
344
|
+
mode: 'unavailable',
|
|
345
|
+
topResidual: [],
|
|
346
|
+
reasonCode: typeof input.reasonCode === 'string' && input.reasonCode.length > 0
|
|
347
|
+
? input.reasonCode
|
|
348
|
+
: STATUS_COMPASS_REASON_CODES.FACTS_UNAVAILABLE,
|
|
349
|
+
reason: typeof input.reason === 'string' && input.reason.length > 0
|
|
350
|
+
? input.reason
|
|
351
|
+
: 'Improvement compass facts are unavailable — run ark-check --doctor for residual lenses. Status never invents green.',
|
|
352
|
+
factsSource: factsSource ?? 'none',
|
|
353
|
+
};
|
|
354
|
+
if (contractHash)
|
|
355
|
+
out.contractHash = contractHash;
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
const topResidual = Array.isArray(input.topResidual)
|
|
359
|
+
? input.topResidual
|
|
360
|
+
.filter((id) => typeof id === 'string' && id.length > 0)
|
|
361
|
+
.slice(0, 15)
|
|
362
|
+
: [];
|
|
363
|
+
const out = {
|
|
364
|
+
schemaVersion: '1.0',
|
|
365
|
+
notAScore: true,
|
|
366
|
+
mode,
|
|
367
|
+
topResidual,
|
|
368
|
+
};
|
|
369
|
+
if (mode === 'subset') {
|
|
370
|
+
out.reasonCode =
|
|
371
|
+
typeof input.reasonCode === 'string' && input.reasonCode.length > 0
|
|
372
|
+
? input.reasonCode
|
|
373
|
+
: STATUS_COMPASS_REASON_CODES.FACTS_PARTIAL;
|
|
374
|
+
out.reason =
|
|
375
|
+
typeof input.reason === 'string' && input.reason.length > 0
|
|
376
|
+
? input.reason
|
|
377
|
+
: 'Status compass is a subset of doctor residual — incomplete session facts; run doctor for full.';
|
|
378
|
+
}
|
|
379
|
+
else if (typeof input.reasonCode === 'string' && input.reasonCode.length > 0) {
|
|
380
|
+
out.reasonCode = input.reasonCode;
|
|
381
|
+
}
|
|
382
|
+
if (typeof input.reason === 'string' && input.reason.length > 0 && mode === 'full') {
|
|
383
|
+
out.reason = input.reason;
|
|
384
|
+
}
|
|
385
|
+
if (factsSource)
|
|
386
|
+
out.factsSource = factsSource;
|
|
387
|
+
if (contractHash)
|
|
388
|
+
out.contractHash = contractHash;
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Unavailable compass when Tooling has no doctor/report residual facts.
|
|
393
|
+
* Empty residual + mode label — never a green / ok claim.
|
|
394
|
+
*/
|
|
395
|
+
export function unavailableStatusImprovementCompass(input = {}) {
|
|
396
|
+
return projectStatusImprovementCompass({
|
|
397
|
+
mode: 'unavailable',
|
|
398
|
+
topResidual: [],
|
|
399
|
+
reasonCode: input.reasonCode ?? STATUS_COMPASS_REASON_CODES.NO_SESSION_SNAPSHOT,
|
|
400
|
+
reason: input.reason ??
|
|
401
|
+
'No session compass facts yet — run ark-check --doctor or --report for residual lenses. Status never invents green.',
|
|
402
|
+
factsSource: 'none',
|
|
403
|
+
contractHash: input.contractHash,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Normalize an incoming status compass slice (Tooling pass-through / snapshot).
|
|
408
|
+
* Rejects score-like shapes; coerces missing mode to subset (never silent full).
|
|
409
|
+
* Unavailable always clears residual.
|
|
410
|
+
*/
|
|
411
|
+
export function normalizeStatusImprovementCompass(value) {
|
|
412
|
+
if (value == null || typeof value !== 'object')
|
|
413
|
+
return null;
|
|
414
|
+
const record = value;
|
|
415
|
+
if (record.notAScore !== true)
|
|
416
|
+
return null;
|
|
417
|
+
if (record.schemaVersion !== '1.0')
|
|
418
|
+
return null;
|
|
419
|
+
// Score-like fields never allowed on status compass.
|
|
420
|
+
if ('score' in record || 'valid' in record || 'goal' in record)
|
|
421
|
+
return null;
|
|
422
|
+
let mode;
|
|
423
|
+
if (typeof record.mode === 'string' && STATUS_COMPASS_MODE_SET.has(record.mode)) {
|
|
424
|
+
mode = record.mode;
|
|
425
|
+
}
|
|
426
|
+
else if (Array.isArray(record.topResidual)) {
|
|
427
|
+
// Legacy thin slice without mode → subset honesty (never silent full).
|
|
428
|
+
mode = 'subset';
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
mode = 'unavailable';
|
|
432
|
+
}
|
|
433
|
+
return projectStatusImprovementCompass({
|
|
434
|
+
mode,
|
|
435
|
+
topResidual: Array.isArray(record.topResidual)
|
|
436
|
+
? record.topResidual
|
|
437
|
+
: [],
|
|
438
|
+
reasonCode: typeof record.reasonCode === 'string' ? record.reasonCode : null,
|
|
439
|
+
reason: typeof record.reason === 'string' ? record.reason : null,
|
|
440
|
+
factsSource: typeof record.factsSource === 'string' ? record.factsSource : null,
|
|
441
|
+
contractHash: typeof record.contractHash === 'string' ? record.contractHash : null,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Residual-id subset check for status ⊆ doctor parity fixtures (DF02).
|
|
446
|
+
* Returns true when every status residual id appears in doctor residual ids.
|
|
447
|
+
*/
|
|
448
|
+
export function statusCompassResidualIsSubsetOfDoctor(statusResidual, doctorResidual) {
|
|
449
|
+
const status = Array.isArray(statusResidual) ? statusResidual : [];
|
|
450
|
+
const doctor = new Set(Array.isArray(doctorResidual) ? doctorResidual : []);
|
|
451
|
+
for (const id of status) {
|
|
452
|
+
if (typeof id !== 'string' || id.length === 0)
|
|
453
|
+
continue;
|
|
454
|
+
if (!doctor.has(id))
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
return true;
|
|
292
458
|
}
|
|
293
459
|
function numberOrNull(value) {
|
|
294
460
|
if (value == null)
|
|
@@ -390,5 +556,25 @@ export const ARK_STATUS_MANIFEST_SCHEMA = {
|
|
|
390
556
|
summary: { type: 'string', minLength: 1 },
|
|
391
557
|
},
|
|
392
558
|
},
|
|
559
|
+
improvementCompass: {
|
|
560
|
+
type: 'object',
|
|
561
|
+
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.',
|
|
562
|
+
additionalProperties: false,
|
|
563
|
+
required: ['schemaVersion', 'notAScore', 'mode', 'topResidual'],
|
|
564
|
+
properties: {
|
|
565
|
+
schemaVersion: { const: '1.0' },
|
|
566
|
+
notAScore: { const: true },
|
|
567
|
+
mode: { enum: ['full', 'subset', 'unavailable'] },
|
|
568
|
+
topResidual: {
|
|
569
|
+
type: 'array',
|
|
570
|
+
items: { type: 'string', minLength: 1 },
|
|
571
|
+
maxItems: 15,
|
|
572
|
+
},
|
|
573
|
+
reasonCode: { type: 'string', minLength: 1 },
|
|
574
|
+
reason: { type: 'string', minLength: 1 },
|
|
575
|
+
factsSource: { enum: ['doctor-facts', 'report-snapshot', 'none'] },
|
|
576
|
+
contractHash: { type: 'string', minLength: 1 },
|
|
577
|
+
},
|
|
578
|
+
},
|
|
393
579
|
},
|
|
394
580
|
};
|