sneakoscope 8.3.2 → 8.6.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/README.md +1 -1
- package/config/architecture-map-policy.v1.json +168 -0
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/commands/bridge.js +19 -1
- package/dist/commands/doctor-profile.js +1 -1
- package/dist/commands/doctor.js +97 -16
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/align/align-route.js +8 -2
- package/dist/core/align/code-navigation-align.js +88 -32
- package/dist/core/architecture-map-pipeline.js +173 -0
- package/dist/core/architecture-map-review.js +180 -0
- package/dist/core/codex/agent-config-file-repair.js +17 -3
- package/dist/core/codex/codex-config-guard.js +3 -1
- package/dist/core/codex/codex-feature-flags.js +10 -0
- package/dist/core/codex/codex-project-config-policy.js +54 -30
- package/dist/core/codex/codex-startup-config-postcheck.js +5 -0
- package/dist/core/codex-app/codex-app-fast-ui-repair.js +2 -1
- package/dist/core/codex-app/menubar/constants.js +1 -0
- package/dist/core/codex-hooks/codex-hook-state-writer.js +2 -0
- package/dist/core/codex-lb/codex-lb-env.js +25 -3
- package/dist/core/codex-lb/combined-catalog/builder.js +7 -2
- package/dist/core/codex-lb/combined-catalog/model-selection.js +71 -0
- package/dist/core/codex-lb/combined-catalog/normalize.js +20 -3
- package/dist/core/codex-lb/desktop-bridge/security.js +11 -8
- package/dist/core/codex-lb/desktop-controller-v3/catalog.js +75 -4
- package/dist/core/codex-lb/desktop-controller-v3.js +5 -1
- package/dist/core/codex-lb/request-route-resolver.js +21 -18
- package/dist/core/commands/triwiki-atlas-command.js +372 -0
- package/dist/core/commands/triwiki-command.js +12 -1
- package/dist/core/doctor/doctor-dirty-planner.js +16 -1
- package/dist/core/doctor/doctor-idempotence.js +11 -0
- package/dist/core/doctor/doctor-repair-postcheck.js +49 -0
- package/dist/core/doctor/doctor-transaction.js +47 -2
- package/dist/core/doctor/legacy-global-hook-cleanup.js +1 -1
- package/dist/core/fsx.js +14 -0
- package/dist/core/git-hygiene/git-policy.js +4 -0
- package/dist/core/git-hygiene/gitignore-writer.js +2 -0
- package/dist/core/gx-renderer.js +2 -1
- package/dist/core/init.js +40 -17
- package/dist/core/managed-paths.js +1 -0
- package/dist/core/perf/release-latency-slo.js +57 -11
- package/dist/core/pipeline-internals/pipeline-stage-builder.js +181 -0
- package/dist/core/pipeline-internals/runtime-core.js +18 -172
- package/dist/core/pipeline-internals/runtime-gates.js +12 -1
- package/dist/core/release/content-digest-cache.js +70 -0
- package/dist/core/release/gate-manifest.js +9 -0
- package/dist/core/release/main-push-guard.js +4 -3
- package/dist/core/release/package-dist-snapshot.js +18 -2
- package/dist/core/release/physical-release-gates.js +26 -3
- package/dist/core/release/release-authorization-snapshot.js +4 -0
- package/dist/core/release/release-gate-cache-v2.js +27 -3
- package/dist/core/release/release-gate-contract.js +2 -0
- package/dist/core/release/release-gate-dag.js +3 -0
- package/dist/core/release/release-inspection-memo.js +69 -0
- package/dist/core/release/release-pack-content-scanner.js +5 -0
- package/dist/core/release/release-pack-receipt.js +28 -18
- package/dist/core/release/release-pack-tarball.js +10 -0
- package/dist/core/release/release-upgrade-baseline.js +4 -0
- package/dist/core/routes.js +1 -1
- package/dist/core/safety/ssot-authority-inventory.js +114 -0
- package/dist/core/safety/ssot-guard.js +2 -44
- package/dist/core/subagents/agent-catalog.js +4 -2
- package/dist/core/subagents/official-subagent-config.js +92 -15
- package/dist/core/subagents/official-subagent-preparation.js +1 -0
- package/dist/core/triwiki/context-graph/architecture/baseline.js +81 -0
- package/dist/core/triwiki/context-graph/architecture/capsule.js +34 -0
- package/dist/core/triwiki/context-graph/architecture/contracts.js +67 -0
- package/dist/core/triwiki/context-graph/architecture/delta.js +149 -0
- package/dist/core/triwiki/context-graph/architecture/finding-factory.js +28 -0
- package/dist/core/triwiki/context-graph/architecture/findings.js +173 -0
- package/dist/core/triwiki/context-graph/architecture/fingerprint.js +75 -0
- package/dist/core/triwiki/context-graph/architecture/index.js +15 -0
- package/dist/core/triwiki/context-graph/architecture/input-bundle.js +66 -0
- package/dist/core/triwiki/context-graph/architecture/metrics.js +137 -0
- package/dist/core/triwiki/context-graph/architecture/policy.js +321 -0
- package/dist/core/triwiki/context-graph/architecture/slice.js +100 -0
- package/dist/core/triwiki/context-graph/architecture/ssot-analysis.js +59 -0
- package/dist/core/triwiki/context-graph/architecture/topology-overlay.js +106 -0
- package/dist/core/triwiki/context-graph/architecture/validation.js +145 -0
- package/dist/core/triwiki/context-graph/architecture/voxel-context.js +39 -0
- package/dist/core/triwiki/context-graph/extractors/index.js +4 -0
- package/dist/core/triwiki/context-graph/projections/index.js +2 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/architecture-delta.js +68 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/ast.js +49 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/change-impact.js +43 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/contracts.js +5 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/escape.js +45 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/ids.js +73 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/index.js +113 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/manifest.js +37 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/module-dependency.js +21 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/ownership-workstream.js +21 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/project-topology.js +13 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/public-surface.js +17 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/reduce.js +44 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/risk-domains.js +23 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/runtime-control.js +20 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/selection-ledger.js +94 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/serializer.js +81 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/ssot-provenance.js +17 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/verification-coverage.js +17 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/view-builder-common.js +1 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/view-builder.js +173 -0
- package/dist/core/triwiki/context-graph/projections/mermaid/view-specs.js +33 -0
- package/dist/core/triwiki/context-graph/store/architecture-map-store.js +71 -0
- package/dist/core/triwiki/triwiki-proof-bank-index.js +26 -1
- package/dist/core/triwiki/triwiki-proof-bank.js +82 -2
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/OverviewViewController.swift +2 -2
- package/dist/native/sks-menubar/Sources/ProvidersModelExposure.swift +178 -0
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +10 -1
- package/dist/scripts/architecture-map-check.js +299 -0
- package/dist/scripts/lib/ensure-dist-fresh.js +48 -1
- package/dist/scripts/release-metadata-check.js +2 -0
- package/package.json +6 -3
- package/release-gates.v2.json +204 -0
- package/schemas/architecture-baseline.v1.schema.json +47 -0
- package/schemas/architecture-map-manifest.v1.schema.json +40 -0
- package/schemas/architecture-map-policy.v1.schema.json +147 -0
- package/schemas/architecture-review.v1.schema.json +46 -0
- package/schemas/mermaid-projection.v1.schema.json +47 -0
|
@@ -22,11 +22,13 @@ export function resolveBridgeRequestRoute(request, policy, options) {
|
|
|
22
22
|
if (options.active_catalog_generation && options.active_catalog_generation !== policy.catalog_generation) {
|
|
23
23
|
return blocked(base, 'catalog_route_index_stale');
|
|
24
24
|
}
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
const threadPin = request.thread_id ? findSessionPin(options.session_pins, request.thread_id) : null;
|
|
26
|
+
const pin = threadPin && canonicalizeBridgeModelId(threadPin.public_model) === model ? threadPin : null;
|
|
27
|
+
const pinAffinity = pin ? sessionPinAffinity(pin, policy, options.route_index, options.registry) : null;
|
|
28
|
+
if (pinAffinity === 'unprovable')
|
|
29
|
+
return blocked(base, 'session_pin_route_unavailable');
|
|
30
|
+
const pinClaim = pinAffinity && pinAffinity !== 'honored' ? pinAffinity : null;
|
|
31
|
+
if (pin && pinAffinity === 'honored') {
|
|
30
32
|
const route = { provider_id: pin.provider_id, upstream_model: pin.upstream_model };
|
|
31
33
|
const providerBlocker = validateProviderRoute(route, request.requested_endpoint_origin, options.registry, options.route_index);
|
|
32
34
|
if (providerBlocker)
|
|
@@ -51,6 +53,8 @@ export function resolveBridgeRequestRoute(request, policy, options) {
|
|
|
51
53
|
return blocked(base, 'catalog_model_route_missing');
|
|
52
54
|
if (!sameTarget(indexed, policyTarget))
|
|
53
55
|
return blocked(base, 'bridge_route_policy_route_index_mismatch');
|
|
56
|
+
if (pinClaim && !sameTarget(indexed, pinClaim))
|
|
57
|
+
return blocked(base, 'session_pin_route_unavailable');
|
|
54
58
|
const providerBlocker = validateProviderRoute(indexed, request.requested_endpoint_origin, options.registry, options.route_index);
|
|
55
59
|
if (providerBlocker)
|
|
56
60
|
return blocked(base, providerBlocker);
|
|
@@ -76,23 +80,22 @@ export function resolveBridgeRequestRoute(request, policy, options) {
|
|
|
76
80
|
recovery_action: null
|
|
77
81
|
};
|
|
78
82
|
}
|
|
79
|
-
function
|
|
80
|
-
if (pin.
|
|
81
|
-
|
|
82
|
-
return 'session_pin_route_unavailable';
|
|
83
|
-
}
|
|
84
|
-
if (!isProviderId(pin.provider_id) || !registry.profiles[pin.provider_id]) {
|
|
85
|
-
return 'session_pin_route_unavailable';
|
|
86
|
-
}
|
|
83
|
+
function sessionPinAffinity(pin, policy, routeIndex, registry) {
|
|
84
|
+
if (!isProviderId(pin.provider_id) || !registry.profiles[pin.provider_id])
|
|
85
|
+
return 'unprovable';
|
|
87
86
|
const model = canonicalizeBridgeModelId(pin.public_model);
|
|
88
87
|
const upstream = normalizeBridgeUpstreamModelId(pin.upstream_model);
|
|
89
88
|
if (!model || !upstream)
|
|
90
|
-
return '
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
93
|
-
|
|
89
|
+
return 'unprovable';
|
|
90
|
+
const claim = { provider_id: pin.provider_id, upstream_model: upstream };
|
|
91
|
+
if (pin.catalog_generation !== policy.catalog_generation
|
|
92
|
+
|| pin.route_policy_generation !== policy.policy_generation) {
|
|
93
|
+
return claim;
|
|
94
94
|
}
|
|
95
|
-
|
|
95
|
+
const indexed = routeIndex.routes[model] || routeIndex.routes[`${pin.provider_id}:${model}`];
|
|
96
|
+
if (!indexed || !sameTarget(indexed, claim))
|
|
97
|
+
return claim;
|
|
98
|
+
return 'honored';
|
|
96
99
|
}
|
|
97
100
|
function validateProviderRoute(route, requestedOrigin, registry, routeIndex) {
|
|
98
101
|
if (!isProviderId(route.provider_id))
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { flag, readOption } from '../../cli/args.js';
|
|
4
|
+
import { ARCHITECTURE_MAP_MANIFEST_SCHEMA, ARCHITECTURE_MAP_VIEW_IDS, GLOBAL_ARCHITECTURE_MAP_VIEW_IDS } from '../triwiki/context-graph/architecture/contracts.js';
|
|
5
|
+
import { ARCHITECTURE_MAP_DIR_REL } from '../triwiki/context-graph/store/architecture-map-store.js';
|
|
6
|
+
export const TRIWIKI_ATLAS_SUBCOMMANDS = [
|
|
7
|
+
'atlas-status',
|
|
8
|
+
'atlas-lint',
|
|
9
|
+
'atlas-list',
|
|
10
|
+
'atlas-show',
|
|
11
|
+
'atlas-why'
|
|
12
|
+
];
|
|
13
|
+
const REPAIR_COMMAND = 'sks align run';
|
|
14
|
+
const VIEW_ID_SET = new Set(ARCHITECTURE_MAP_VIEW_IDS);
|
|
15
|
+
const GLOBAL_VIEW_ID_SET = new Set(GLOBAL_ARCHITECTURE_MAP_VIEW_IDS);
|
|
16
|
+
export function isTriWikiAtlasSubcommand(value) {
|
|
17
|
+
return typeof value === 'string' && TRIWIKI_ATLAS_SUBCOMMANDS.includes(value);
|
|
18
|
+
}
|
|
19
|
+
function mapRoot(root) {
|
|
20
|
+
return path.join(root, ...ARCHITECTURE_MAP_DIR_REL.split('/'));
|
|
21
|
+
}
|
|
22
|
+
function missionMapRoot(root, missionId) {
|
|
23
|
+
return path.join(root, '.sneakoscope', 'missions', missionId, 'architecture-map');
|
|
24
|
+
}
|
|
25
|
+
function resolveMapDir(root, args) {
|
|
26
|
+
const missionId = readOption(args, '--mission', '') || null;
|
|
27
|
+
if (missionId) {
|
|
28
|
+
return { dir: missionMapRoot(root, missionId), mode: 'mission', missionId };
|
|
29
|
+
}
|
|
30
|
+
return { dir: mapRoot(root), mode: 'global', missionId: null };
|
|
31
|
+
}
|
|
32
|
+
function readJsonFile(file) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function loadManifest(dir) {
|
|
41
|
+
const file = path.join(dir, 'manifest.json');
|
|
42
|
+
if (!fs.existsSync(file))
|
|
43
|
+
return { ok: false, manifest: null, path: file };
|
|
44
|
+
const manifest = readJsonFile(file);
|
|
45
|
+
return { ok: Boolean(manifest && manifest.schema === ARCHITECTURE_MAP_MANIFEST_SCHEMA), manifest, path: file };
|
|
46
|
+
}
|
|
47
|
+
function loadFindings(dir) {
|
|
48
|
+
const file = path.join(dir, 'findings.json');
|
|
49
|
+
const doc = readJsonFile(file);
|
|
50
|
+
return Array.isArray(doc?.findings) ? doc.findings : [];
|
|
51
|
+
}
|
|
52
|
+
function countBySeverity(findings) {
|
|
53
|
+
let blocking = 0;
|
|
54
|
+
let warning = 0;
|
|
55
|
+
let info = 0;
|
|
56
|
+
for (const finding of findings) {
|
|
57
|
+
if (finding.severity === 'blocking')
|
|
58
|
+
blocking += 1;
|
|
59
|
+
else if (finding.severity === 'warning')
|
|
60
|
+
warning += 1;
|
|
61
|
+
else
|
|
62
|
+
info += 1;
|
|
63
|
+
}
|
|
64
|
+
return { blocking, warning, info };
|
|
65
|
+
}
|
|
66
|
+
function graphFreshness(root, manifest) {
|
|
67
|
+
if (!manifest)
|
|
68
|
+
return 'missing';
|
|
69
|
+
const graphPath = path.join(root, '.sneakoscope', 'wiki', 'context-graph.json');
|
|
70
|
+
if (!fs.existsSync(graphPath))
|
|
71
|
+
return 'missing';
|
|
72
|
+
const graph = readJsonFile(graphPath);
|
|
73
|
+
const expected = String(graph?.snapshotHash || '');
|
|
74
|
+
const observed = String(manifest.graphHash || manifest.sourceBinding?.graphHash || '');
|
|
75
|
+
if (!expected || !observed)
|
|
76
|
+
return 'missing';
|
|
77
|
+
return expected === observed ? 'fresh' : 'stale';
|
|
78
|
+
}
|
|
79
|
+
async function atlasStatus(root, args) {
|
|
80
|
+
const resolved = resolveMapDir(root, args);
|
|
81
|
+
const loaded = loadManifest(resolved.dir);
|
|
82
|
+
const findings = loaded.manifest ? loadFindings(resolved.dir) : [];
|
|
83
|
+
const severity = countBySeverity(findings);
|
|
84
|
+
const freshness = graphFreshness(root, loaded.manifest);
|
|
85
|
+
const ok = loaded.ok && freshness === 'fresh' && severity.blocking === 0;
|
|
86
|
+
const result = {
|
|
87
|
+
schema: 'sks.triwiki-atlas-status.v1',
|
|
88
|
+
ok,
|
|
89
|
+
mode: resolved.mode,
|
|
90
|
+
mission_id: resolved.missionId,
|
|
91
|
+
manifest_schema: loaded.manifest?.schema ?? null,
|
|
92
|
+
graph_hash: loaded.manifest?.graphHash ?? null,
|
|
93
|
+
policy_hash: loaded.manifest?.policyHash ?? null,
|
|
94
|
+
generated_from: loaded.manifest?.canonicalHash ?? null,
|
|
95
|
+
freshness,
|
|
96
|
+
view_count: loaded.manifest?.views?.length ?? 0,
|
|
97
|
+
blocking_count: severity.blocking,
|
|
98
|
+
warning_count: severity.warning,
|
|
99
|
+
projection_accounting: loaded.manifest?.projectionAccounting ?? null,
|
|
100
|
+
next_action: ok ? null : REPAIR_COMMAND,
|
|
101
|
+
repair_command: REPAIR_COMMAND
|
|
102
|
+
};
|
|
103
|
+
const lines = [
|
|
104
|
+
`Architecture map (${resolved.mode}): ${ok ? 'ok' : 'blocked'}`,
|
|
105
|
+
`Freshness: ${freshness}`,
|
|
106
|
+
`Views: ${result.view_count}; blocking=${severity.blocking} warning=${severity.warning}`,
|
|
107
|
+
...(ok ? [] : [`Repair: ${REPAIR_COMMAND}`])
|
|
108
|
+
];
|
|
109
|
+
return { result, ok, lines };
|
|
110
|
+
}
|
|
111
|
+
async function atlasLint(root, args) {
|
|
112
|
+
const resolved = resolveMapDir(root, args);
|
|
113
|
+
const loaded = loadManifest(resolved.dir);
|
|
114
|
+
const errors = [];
|
|
115
|
+
if (!loaded.manifest)
|
|
116
|
+
errors.push('manifest_missing');
|
|
117
|
+
else {
|
|
118
|
+
if (loaded.manifest.schema !== ARCHITECTURE_MAP_MANIFEST_SCHEMA)
|
|
119
|
+
errors.push('schema');
|
|
120
|
+
if (!loaded.manifest.graphHash)
|
|
121
|
+
errors.push('graph_hash');
|
|
122
|
+
if (!loaded.manifest.policyHash)
|
|
123
|
+
errors.push('policy_hash');
|
|
124
|
+
if (!Array.isArray(loaded.manifest.views) || loaded.manifest.views.length === 0)
|
|
125
|
+
errors.push('views');
|
|
126
|
+
if (!loaded.manifest.projectionAccounting)
|
|
127
|
+
errors.push('projection_accounting');
|
|
128
|
+
for (const view of loaded.manifest.views ?? []) {
|
|
129
|
+
if (!VIEW_ID_SET.has(view.viewId))
|
|
130
|
+
errors.push(`unknown_view:${view.viewId}`);
|
|
131
|
+
const viewPath = path.join(resolved.dir, 'views', `${view.viewId}.mmd`);
|
|
132
|
+
if (!fs.existsSync(viewPath)) {
|
|
133
|
+
errors.push(`missing_view_file:${view.viewId}`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const text = fs.readFileSync(viewPath, 'utf8');
|
|
137
|
+
if (!text.startsWith('%% GENERATED BY SKS'))
|
|
138
|
+
errors.push(`header:${view.viewId}`);
|
|
139
|
+
if (!/\bflowchart (TD|LR)\b/.test(text))
|
|
140
|
+
errors.push(`grammar:${view.viewId}`);
|
|
141
|
+
if (/\/Users\/|\/home\/|C:\\\\/.test(text))
|
|
142
|
+
errors.push(`absolute_path:${view.viewId}`);
|
|
143
|
+
const rel = path.relative(resolved.dir, viewPath);
|
|
144
|
+
if (rel.startsWith('..') || path.isAbsolute(rel))
|
|
145
|
+
errors.push(`path_escape:${view.viewId}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const freshness = graphFreshness(root, loaded.manifest);
|
|
149
|
+
if (freshness !== 'fresh')
|
|
150
|
+
errors.push(`freshness_${freshness}`);
|
|
151
|
+
const ok = errors.length === 0;
|
|
152
|
+
const result = {
|
|
153
|
+
schema: 'sks.triwiki-atlas-lint.v1',
|
|
154
|
+
ok,
|
|
155
|
+
mode: resolved.mode,
|
|
156
|
+
mission_id: resolved.missionId,
|
|
157
|
+
error_count: errors.length,
|
|
158
|
+
errors,
|
|
159
|
+
repair_command: REPAIR_COMMAND
|
|
160
|
+
};
|
|
161
|
+
return {
|
|
162
|
+
result,
|
|
163
|
+
ok,
|
|
164
|
+
lines: [
|
|
165
|
+
`Architecture map lint: ${ok ? 'ok' : 'blocked'} (${errors.length} errors)`,
|
|
166
|
+
...errors.slice(0, 20).map((error) => `- ${error}`),
|
|
167
|
+
...(ok ? [] : [`Repair: ${REPAIR_COMMAND}`])
|
|
168
|
+
]
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
async function atlasList(root, args) {
|
|
172
|
+
const resolved = resolveMapDir(root, args);
|
|
173
|
+
const loaded = loadManifest(resolved.dir);
|
|
174
|
+
if (!loaded.manifest) {
|
|
175
|
+
return {
|
|
176
|
+
result: {
|
|
177
|
+
schema: 'sks.triwiki-atlas-list.v1',
|
|
178
|
+
ok: false,
|
|
179
|
+
error_code: 'manifest_missing',
|
|
180
|
+
repair_command: REPAIR_COMMAND
|
|
181
|
+
},
|
|
182
|
+
ok: false,
|
|
183
|
+
lines: ['Architecture map list: manifest_missing', `Repair: ${REPAIR_COMMAND}`]
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const views = (loaded.manifest.views ?? []).map((view) => {
|
|
187
|
+
const rel = `${ARCHITECTURE_MAP_DIR_REL}/views/${view.viewId}.mmd`;
|
|
188
|
+
const abs = path.join(resolved.dir, 'views', `${view.viewId}.mmd`);
|
|
189
|
+
let direction = null;
|
|
190
|
+
let nodeCount = 0;
|
|
191
|
+
let edgeCount = 0;
|
|
192
|
+
if (fs.existsSync(abs)) {
|
|
193
|
+
const text = fs.readFileSync(abs, 'utf8');
|
|
194
|
+
direction = text.match(/\bflowchart (TD|LR)\b/)?.[1] ?? null;
|
|
195
|
+
nodeCount = [...text.matchAll(/^\s*[A-Za-z0-9_]+\[/gm)].length;
|
|
196
|
+
edgeCount = [...text.matchAll(/-->/g)].length;
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
view_id: view.viewId,
|
|
200
|
+
path: rel,
|
|
201
|
+
direction,
|
|
202
|
+
node_count: nodeCount,
|
|
203
|
+
edge_count: edgeCount,
|
|
204
|
+
byte_count: view.byteLength,
|
|
205
|
+
hash: view.contentHash
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
const result = {
|
|
209
|
+
schema: 'sks.triwiki-atlas-list.v1',
|
|
210
|
+
ok: true,
|
|
211
|
+
mode: resolved.mode,
|
|
212
|
+
mission_id: resolved.missionId,
|
|
213
|
+
views
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
result,
|
|
217
|
+
ok: true,
|
|
218
|
+
lines: views.map((view) => `${view.view_id} ${view.direction ?? '?'} nodes=${view.node_count} edges=${view.edge_count} bytes=${view.byte_count} ${view.hash.slice(0, 12)}`)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
async function atlasShow(root, args) {
|
|
222
|
+
const viewId = args.find((arg) => !arg.startsWith('-')) ?? '';
|
|
223
|
+
if (!viewId || !VIEW_ID_SET.has(viewId)) {
|
|
224
|
+
return {
|
|
225
|
+
result: {
|
|
226
|
+
schema: 'sks.triwiki-atlas-show.v1',
|
|
227
|
+
ok: false,
|
|
228
|
+
error_code: 'invalid_view_id',
|
|
229
|
+
allowed: [...ARCHITECTURE_MAP_VIEW_IDS]
|
|
230
|
+
},
|
|
231
|
+
ok: false,
|
|
232
|
+
lines: [
|
|
233
|
+
'Usage: sks triwiki atlas-show <view-id> [--mission <id>] [--format mermaid|json]',
|
|
234
|
+
`Allowed: ${ARCHITECTURE_MAP_VIEW_IDS.join(', ')}`
|
|
235
|
+
]
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (!GLOBAL_VIEW_ID_SET.has(viewId) && !readOption(args, '--mission', '')) {
|
|
239
|
+
return {
|
|
240
|
+
result: {
|
|
241
|
+
schema: 'sks.triwiki-atlas-show.v1',
|
|
242
|
+
ok: false,
|
|
243
|
+
error_code: 'mission_view_requires_mission',
|
|
244
|
+
view_id: viewId
|
|
245
|
+
},
|
|
246
|
+
ok: false,
|
|
247
|
+
lines: [`View ${viewId} is mission-scoped; pass --mission <id>`]
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const format = readOption(args, '--format', 'mermaid') || 'mermaid';
|
|
251
|
+
const resolved = resolveMapDir(root, args);
|
|
252
|
+
const viewPath = path.join(resolved.dir, 'views', `${viewId}.mmd`);
|
|
253
|
+
const confined = path.relative(resolved.dir, viewPath);
|
|
254
|
+
if (confined.startsWith('..') || path.isAbsolute(confined)) {
|
|
255
|
+
return {
|
|
256
|
+
result: { schema: 'sks.triwiki-atlas-show.v1', ok: false, error_code: 'path_escape' },
|
|
257
|
+
ok: false,
|
|
258
|
+
lines: ['Architecture map show blocked: path_escape']
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (!fs.existsSync(viewPath)) {
|
|
262
|
+
return {
|
|
263
|
+
result: {
|
|
264
|
+
schema: 'sks.triwiki-atlas-show.v1',
|
|
265
|
+
ok: false,
|
|
266
|
+
error_code: 'view_missing',
|
|
267
|
+
view_id: viewId,
|
|
268
|
+
repair_command: REPAIR_COMMAND
|
|
269
|
+
},
|
|
270
|
+
ok: false,
|
|
271
|
+
lines: [`Architecture map show: view_missing (${viewId})`, `Repair: ${REPAIR_COMMAND}`]
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const text = fs.readFileSync(viewPath, 'utf8');
|
|
275
|
+
if (format === 'json') {
|
|
276
|
+
const result = {
|
|
277
|
+
schema: 'sks.triwiki-atlas-show.v1',
|
|
278
|
+
ok: true,
|
|
279
|
+
view_id: viewId,
|
|
280
|
+
format: 'json',
|
|
281
|
+
path: `${ARCHITECTURE_MAP_DIR_REL}/views/${viewId}.mmd`,
|
|
282
|
+
text
|
|
283
|
+
};
|
|
284
|
+
return { result, ok: true, lines: [JSON.stringify(result, null, 2)] };
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
result: {
|
|
288
|
+
schema: 'sks.triwiki-atlas-show.v1',
|
|
289
|
+
ok: true,
|
|
290
|
+
view_id: viewId,
|
|
291
|
+
format: 'mermaid',
|
|
292
|
+
path: `${ARCHITECTURE_MAP_DIR_REL}/views/${viewId}.mmd`,
|
|
293
|
+
text
|
|
294
|
+
},
|
|
295
|
+
ok: true,
|
|
296
|
+
lines: [text.trimEnd()]
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
async function atlasWhy(root, args) {
|
|
300
|
+
const findingId = args.find((arg) => !arg.startsWith('-')) ?? '';
|
|
301
|
+
const missionId = readOption(args, '--mission', '');
|
|
302
|
+
if (!findingId || !missionId) {
|
|
303
|
+
return {
|
|
304
|
+
result: { schema: 'sks.triwiki-atlas-why.v1', ok: false, error_code: 'usage' },
|
|
305
|
+
ok: false,
|
|
306
|
+
lines: ['Usage: sks triwiki atlas-why <finding-id> --mission <id> [--json]']
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const resolved = resolveMapDir(root, [`--mission`, missionId]);
|
|
310
|
+
const findings = loadFindings(resolved.dir);
|
|
311
|
+
const finding = findings.find((entry) => entry.id === findingId) ?? null;
|
|
312
|
+
if (!finding) {
|
|
313
|
+
return {
|
|
314
|
+
result: {
|
|
315
|
+
schema: 'sks.triwiki-atlas-why.v1',
|
|
316
|
+
ok: false,
|
|
317
|
+
error_code: 'finding_not_found',
|
|
318
|
+
finding_id: findingId,
|
|
319
|
+
mission_id: missionId
|
|
320
|
+
},
|
|
321
|
+
ok: false,
|
|
322
|
+
lines: [`Architecture map why: finding_not_found (${findingId})`]
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const result = {
|
|
326
|
+
schema: 'sks.triwiki-atlas-why.v1',
|
|
327
|
+
ok: true,
|
|
328
|
+
finding_id: finding.id,
|
|
329
|
+
code: finding.code ?? null,
|
|
330
|
+
severity: finding.severity ?? null,
|
|
331
|
+
subject: finding.subjectIds ?? [],
|
|
332
|
+
evidence_path: finding.evidenceIds ?? [],
|
|
333
|
+
before: null,
|
|
334
|
+
after: null,
|
|
335
|
+
policy_rule: finding.ruleId ?? null,
|
|
336
|
+
exception: finding.disposition ?? null,
|
|
337
|
+
inspect_nodes: finding.subjectIds ?? [],
|
|
338
|
+
message: finding.message ?? null
|
|
339
|
+
};
|
|
340
|
+
return {
|
|
341
|
+
result,
|
|
342
|
+
ok: true,
|
|
343
|
+
lines: [
|
|
344
|
+
`${finding.id}: ${finding.code ?? 'unknown'} (${finding.severity ?? 'info'})`,
|
|
345
|
+
...(finding.subjectIds?.length ? [`Subject: ${finding.subjectIds.join(', ')}`] : []),
|
|
346
|
+
...(finding.message ? [finding.message] : [])
|
|
347
|
+
]
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
export async function triwikiAtlasCommand(root, sub, args) {
|
|
351
|
+
let answer;
|
|
352
|
+
if (sub === 'atlas-status')
|
|
353
|
+
answer = await atlasStatus(root, args);
|
|
354
|
+
else if (sub === 'atlas-lint')
|
|
355
|
+
answer = await atlasLint(root, args);
|
|
356
|
+
else if (sub === 'atlas-list')
|
|
357
|
+
answer = await atlasList(root, args);
|
|
358
|
+
else if (sub === 'atlas-show')
|
|
359
|
+
answer = await atlasShow(root, args);
|
|
360
|
+
else
|
|
361
|
+
answer = await atlasWhy(root, args);
|
|
362
|
+
const usageFailed = typeof answer.result === 'object' &&
|
|
363
|
+
answer.result !== null &&
|
|
364
|
+
'error_code' in answer.result &&
|
|
365
|
+
answer.result.error_code === 'usage';
|
|
366
|
+
const invalidView = typeof answer.result === 'object' &&
|
|
367
|
+
answer.result !== null &&
|
|
368
|
+
'error_code' in answer.result &&
|
|
369
|
+
answer.result.error_code === 'invalid_view_id';
|
|
370
|
+
const exitCode = answer.ok ? 0 : usageFailed || invalidView ? 2 : 1;
|
|
371
|
+
return { ...answer, exitCode };
|
|
372
|
+
}
|
|
@@ -5,6 +5,7 @@ import { computeTriWikiAffectedGraph } from '../triwiki/triwiki-affected-graph.j
|
|
|
5
5
|
import { buildTriWikiGateImpactMap } from '../triwiki/triwiki-gate-impact-map.js';
|
|
6
6
|
import { DEFAULT_TRIWIKI_MODULE_CARDS } from '../triwiki/triwiki-module-card.js';
|
|
7
7
|
import { summarizeTriWikiProofBank } from '../triwiki/triwiki-proof-bank.js';
|
|
8
|
+
import { isTriWikiAtlasSubcommand, triwikiAtlasCommand } from './triwiki-atlas-command.js';
|
|
8
9
|
import { isTriWikiGraphSubcommand, triwikiGraphCommand } from './triwiki-graph-command.js';
|
|
9
10
|
export async function triwikiCommand(args = []) {
|
|
10
11
|
const root = await projectRoot();
|
|
@@ -20,6 +21,16 @@ export async function triwikiCommand(args = []) {
|
|
|
20
21
|
console.log(line);
|
|
21
22
|
return answer.result;
|
|
22
23
|
}
|
|
24
|
+
if (isTriWikiAtlasSubcommand(sub)) {
|
|
25
|
+
const answer = await triwikiAtlasCommand(root, sub, args.slice(1));
|
|
26
|
+
process.exitCode = answer.exitCode;
|
|
27
|
+
if (json)
|
|
28
|
+
return printJson(answer.result);
|
|
29
|
+
const stream = answer.ok ? console.log : console.error;
|
|
30
|
+
for (const line of answer.lines)
|
|
31
|
+
stream(line);
|
|
32
|
+
return answer.result;
|
|
33
|
+
}
|
|
23
34
|
if (sub === 'index') {
|
|
24
35
|
result = {
|
|
25
36
|
schema: 'sks.triwiki-index.v1',
|
|
@@ -36,7 +47,7 @@ export async function triwikiCommand(args = []) {
|
|
|
36
47
|
result = summarizeTriWikiProofBank(root);
|
|
37
48
|
}
|
|
38
49
|
else {
|
|
39
|
-
console.error('Usage: sks triwiki index|affected|proof-bank|graph-status|graph-lint|graph-query [--json]');
|
|
50
|
+
console.error('Usage: sks triwiki index|affected|proof-bank|graph-status|graph-lint|graph-query|atlas-status|atlas-lint|atlas-list|atlas-show|atlas-why [--json]');
|
|
40
51
|
process.exitCode = 1;
|
|
41
52
|
return null;
|
|
42
53
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { spawnSync } from 'node:child_process';
|
|
4
5
|
import { MANAGED_ASSET_VERSION } from '../managed-assets/managed-assets-manifest.js';
|
|
@@ -104,7 +105,21 @@ function phaseInputHash(root, id) {
|
|
|
104
105
|
return { rel, hash: 'missing' };
|
|
105
106
|
return hashSemanticPath(root, rel);
|
|
106
107
|
});
|
|
107
|
-
return hashJson({
|
|
108
|
+
return hashJson({
|
|
109
|
+
id,
|
|
110
|
+
package: packageIdentity(),
|
|
111
|
+
files,
|
|
112
|
+
env: phaseEnvPresence(id),
|
|
113
|
+
semantic_state: phaseSemanticState(root, id),
|
|
114
|
+
project_codex_config: hashSemanticPath(root, '.codex/config.toml').hash,
|
|
115
|
+
codex_home_config: hashFileIfExists(codexHomeConfigPath()),
|
|
116
|
+
phase_schema_version: 3
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function codexHomeConfigPath() {
|
|
120
|
+
const codexHome = process.env.CODEX_HOME
|
|
121
|
+
|| path.join(process.env.HOME || os.homedir(), '.codex');
|
|
122
|
+
return path.join(codexHome, 'config.toml');
|
|
108
123
|
}
|
|
109
124
|
function phaseInputFiles(id) {
|
|
110
125
|
if (id.includes('context7'))
|
|
@@ -17,6 +17,8 @@ export async function runDoctorIdempotence(root) {
|
|
|
17
17
|
blockers.push(`second_doctor_exit_${second.exit_code}`);
|
|
18
18
|
if (changedFilesSecond.length > 0)
|
|
19
19
|
blockers.push('second_doctor_not_noop');
|
|
20
|
+
if (!reportsChangedFiles(second))
|
|
21
|
+
blockers.push('second_doctor_changed_files_unavailable');
|
|
20
22
|
const report = {
|
|
21
23
|
schema: 'sks.doctor-idempotence.v1',
|
|
22
24
|
ok: blockers.length === 0,
|
|
@@ -60,6 +62,15 @@ async function runDoctor(root, home, codexHome) {
|
|
|
60
62
|
parsed
|
|
61
63
|
};
|
|
62
64
|
}
|
|
65
|
+
function reportsChangedFiles(run) {
|
|
66
|
+
if (!run.parsed)
|
|
67
|
+
return false;
|
|
68
|
+
return [
|
|
69
|
+
run.parsed?.repair?.doctor_transaction?.changed_files,
|
|
70
|
+
run.parsed?.doctor_fix_transaction?.changed_files,
|
|
71
|
+
run.parsed?.changed_files
|
|
72
|
+
].some((value) => Array.isArray(value));
|
|
73
|
+
}
|
|
63
74
|
function extractChangedFiles(run) {
|
|
64
75
|
const repair = run.parsed?.repair || {};
|
|
65
76
|
const candidates = [
|
|
@@ -1,4 +1,53 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { inspectOfficialSubagentToml, readOfficialSubagentConfig } from '../subagents/official-subagent-config.js';
|
|
1
5
|
import {} from './doctor-transaction.js';
|
|
6
|
+
export async function verifyCodexConfigsOnDisk(input) {
|
|
7
|
+
const root = path.resolve(input.root);
|
|
8
|
+
const home = input.home || process.env.HOME || os.homedir();
|
|
9
|
+
const codexHome = input.codexHome || process.env.CODEX_HOME || path.join(home, '.codex');
|
|
10
|
+
const projectConfigPath = path.join(root, '.codex', 'config.toml');
|
|
11
|
+
const codexHomeConfigPath = path.join(codexHome, 'config.toml');
|
|
12
|
+
const blockers = [];
|
|
13
|
+
const [projectText, homeText] = await Promise.all([
|
|
14
|
+
fs.readFile(projectConfigPath, 'utf8').catch(() => null),
|
|
15
|
+
fs.readFile(codexHomeConfigPath, 'utf8').catch(() => null)
|
|
16
|
+
]);
|
|
17
|
+
if (projectText !== null && !inspectOfficialSubagentToml(projectText).ok) {
|
|
18
|
+
blockers.push('project_codex_config_unparseable_after_repair');
|
|
19
|
+
}
|
|
20
|
+
if (homeText !== null && !inspectOfficialSubagentToml(homeText).ok) {
|
|
21
|
+
blockers.push('codex_home_config_unparseable_after_repair');
|
|
22
|
+
}
|
|
23
|
+
let multiAgentV2Enabled = null;
|
|
24
|
+
let agentsEnabled = null;
|
|
25
|
+
if (!blockers.length) {
|
|
26
|
+
const config = await readOfficialSubagentConfig(root, { home, codexHome }).catch(() => null);
|
|
27
|
+
if (config) {
|
|
28
|
+
multiAgentV2Enabled = config.multiAgentV2.enabled;
|
|
29
|
+
agentsEnabled = config.enabled;
|
|
30
|
+
for (const blocker of config.blockers)
|
|
31
|
+
blockers.push(`official_subagent_config:${blocker}`);
|
|
32
|
+
if (multiAgentV2Enabled === false) {
|
|
33
|
+
blockers.push('official_subagent_multi_agent_v2_disabled_after_repair');
|
|
34
|
+
if (agentsEnabled === false)
|
|
35
|
+
blockers.push('official_subagent_agents_disabled_after_repair');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
schema: 'sks.doctor-config-disk-verification.v1',
|
|
41
|
+
ok: blockers.length === 0,
|
|
42
|
+
project_config_path: projectConfigPath,
|
|
43
|
+
codex_home_config_path: codexHomeConfigPath,
|
|
44
|
+
project_config_present: projectText !== null,
|
|
45
|
+
codex_home_config_present: homeText !== null,
|
|
46
|
+
multi_agent_v2_enabled: multiAgentV2Enabled,
|
|
47
|
+
agents_enabled: agentsEnabled,
|
|
48
|
+
blockers: [...new Set(blockers)]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
2
51
|
export function doctorRepairPostcheck(transaction) {
|
|
3
52
|
const phases = transaction?.phases || [];
|
|
4
53
|
const requiredBlockers = phases
|
|
@@ -3,6 +3,47 @@ import { nowIso, writeJsonAtomic } from '../fsx.js';
|
|
|
3
3
|
import { isDoctorPhaseClean, markDoctorPhaseClean } from './doctor-dirty-planner.js';
|
|
4
4
|
import { ui as cliUi } from '../../cli/cli-theme.js';
|
|
5
5
|
import { messageOf } from '../errors/message.js';
|
|
6
|
+
const CHANGED_PATH_KEYS = Object.freeze([
|
|
7
|
+
'repaired_paths', 'created_files', 'created', 'updated', 'changed_files',
|
|
8
|
+
'written_files', 'removed_files', 'removed', 'quarantined', 'backups'
|
|
9
|
+
]);
|
|
10
|
+
export function changedFilesFromRepairReport(report) {
|
|
11
|
+
const found = new Set();
|
|
12
|
+
const walk = (value, depth) => {
|
|
13
|
+
if (depth > 6 || !value || typeof value !== 'object')
|
|
14
|
+
return;
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
for (const item of value)
|
|
17
|
+
walk(item, depth + 1);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const row = value;
|
|
21
|
+
for (const key of CHANGED_PATH_KEYS) {
|
|
22
|
+
const entries = row[key];
|
|
23
|
+
if (Array.isArray(entries)) {
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const text = String(entry || '').trim();
|
|
26
|
+
if (text)
|
|
27
|
+
found.add(text);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else if (typeof entries === 'string' && entries.trim()) {
|
|
31
|
+
found.add(entries.trim());
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (row.changed === true || row.applied === true) {
|
|
35
|
+
for (const key of ['config_path', 'backup_path', 'path', 'file']) {
|
|
36
|
+
const text = String(row[key] || '').trim();
|
|
37
|
+
if (text)
|
|
38
|
+
found.add(text);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const nested of Object.values(row))
|
|
42
|
+
walk(nested, depth + 1);
|
|
43
|
+
};
|
|
44
|
+
walk(report, 0);
|
|
45
|
+
return [...found].sort();
|
|
46
|
+
}
|
|
6
47
|
export async function runDoctorFixTransaction(input) {
|
|
7
48
|
const startedAt = nowIso();
|
|
8
49
|
const phases = [];
|
|
@@ -110,7 +151,8 @@ export async function writeDoctorFixTransaction(input) {
|
|
|
110
151
|
started_at: phase.started_at || null,
|
|
111
152
|
completed_at: phase.completed_at || null,
|
|
112
153
|
duration_ms: Number.isFinite(phase.duration_ms) ? Number(phase.duration_ms) : null,
|
|
113
|
-
rollback_performed: phase.rollback_performed === true
|
|
154
|
+
rollback_performed: phase.rollback_performed === true,
|
|
155
|
+
changed_files: [...new Set(phase.changed_files || [])].sort()
|
|
114
156
|
}));
|
|
115
157
|
const postcheckOk = phases.every((phase) => phase.ok || phase.required_for_ready === false);
|
|
116
158
|
const mutationsWithoutRollback = phases.filter((phase) => phase.required_for_ready && phase.repaired && !phase.rollback_evidence).length;
|
|
@@ -124,6 +166,7 @@ export async function writeDoctorFixTransaction(input) {
|
|
|
124
166
|
postcheck_ok: postcheckOk && mutationsWithoutRollback === 0,
|
|
125
167
|
rollback_performed: input.rollbackPerformed === true,
|
|
126
168
|
mutations_without_rollback: mutationsWithoutRollback,
|
|
169
|
+
changed_files: [...new Set(phases.flatMap((phase) => phase.changed_files))].sort(),
|
|
127
170
|
raw_secret_values_recorded: false,
|
|
128
171
|
skipped_clean_phases: phases.filter((phase) => phase.warnings.some((warning) => warning.startsWith('dirty_plan_skipped_clean_phase'))).map((phase) => phase.id),
|
|
129
172
|
dirty_phases: input.dirtyPlan?.phases.filter((phase) => phase.status === 'dirty').map((phase) => phase.id) || phases.filter((phase) => !phase.warnings.some((warning) => warning.startsWith('dirty_plan_skipped_clean_phase'))).map((phase) => phase.id),
|
|
@@ -158,7 +201,9 @@ function normalizePhase(definition, result, fallback, startedMs) {
|
|
|
158
201
|
started_at: phase.started_at || fallback.started_at || nowIso(),
|
|
159
202
|
completed_at: phase.completed_at || nowIso(),
|
|
160
203
|
duration_ms: phase.duration_ms ?? Math.max(0, Date.now() - startedMs),
|
|
161
|
-
rollback_performed: phase.rollback_performed === true
|
|
204
|
+
rollback_performed: phase.rollback_performed === true,
|
|
205
|
+
changed_files: phase.changed_files
|
|
206
|
+
?? (definition.report ? changedFilesFromRepairReport(definition.report()) : [])
|
|
162
207
|
};
|
|
163
208
|
}
|
|
164
209
|
function mergePhase(phase, update) {
|
|
@@ -144,7 +144,7 @@ function projectSksHookCommand(value) {
|
|
|
144
144
|
const command = String(value || '').trim();
|
|
145
145
|
if (/[;&|`$<>\r\n]/.test(command))
|
|
146
146
|
return null;
|
|
147
|
-
return command.match(/^node\s
|
|
147
|
+
return command.match(/^(?:\S*[\\/])?(?:node\s+\S*sks\.js|sks)\s+hook\s+([a-z0-9-]+)$/i)?.[1] || null;
|
|
148
148
|
}
|
|
149
149
|
function hookRef(event, matcher, command) {
|
|
150
150
|
return `${event}:${String(matcher ?? '')}:${command}`;
|