@ngockhoale/ukit 3.4.69 → 3.4.70
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 +10 -0
- package/manifests/documentation.yaml +88 -0
- package/manifests/platform.full.yaml +13 -1
- package/package.json +5 -1
- package/scripts/install/module-dependency-closure.mjs +53 -0
- package/scripts/install/sync-installed-mirror.mjs +17 -4
- package/scripts/release/candidate-state.mjs +31 -0
- package/scripts/release/verify-release.mjs +60 -6
- package/src/core/buildPlan.js +2 -0
- package/src/core/executionContracts.js +1 -309
- package/template_project/.claude/commands/ukit/handoff-create.md +41 -1
- package/template_project/.claude/hooks/handoff-model-guard.sh +459 -1
- package/template_project/.claude/hooks/skill-router.sh +585 -357
- package/template_project/.claude/ukit/index/publication-intent.mjs +528 -0
- package/template_project/.claude/ukit/index/route-resolver.mjs +1 -290
- package/template_project/.claude/ukit/index/route-task.mjs +13 -1
- package/template_project/.claude/ukit/runtime/block-breaker.mjs +232 -1
- package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +46 -8
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +161 -56
- package/template_project/.claude/ukit/runtime/hook-chain-runner.mjs +51 -1
- package/template_project/.claude/ukit/runtime/operation-evidence.mjs +34 -10
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +357 -44
- package/template_project/.omp/hooks/pre/ukit-bridge.js +50 -26
- package/scripts/dev/capture-omp-stall.mjs +0 -246
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 3.4.70 - 2026-10-26
|
|
6
|
+
|
|
7
|
+
**Task-owned completion evidence and bounded reminder recovery; no native-host certification.**
|
|
8
|
+
|
|
9
|
+
- Classification: configuration comparisons, read-only output specifications and owner-approval reminders do not inherit mutation or verification debt. Explicit verification and publication orders retain their own obligations; private Git scope and later affirmative orders remain distinct. Descriptive acceptance-review prose — subordinator continuation fragments ("…, including source fixes F-3") and non-imperative goal/acceptance lines ("Final product goal: … normal ukit update pass") — no longer routes read-only reviews as implementation orders; any imperative fragment inside such a line still enforces write+verification.
|
|
10
|
+
- Completion: preserve truthful blocked outcomes, scoped exhausted-denial reconciliation and native Git/remote evidence. Launch acknowledgements, missing exit proof, masked commands, foreign receipts and stale parity cannot prove success. Docs-only Handoff creation honors the owner's explicit Git publication scope.
|
|
11
|
+
- Reminder recovery: reapply classified evidence obligations even when the task-kind signature is unchanged. Repeated read-only reminders need no artificial checkpoint verification; owned external blockers remain blocked rather than successful. No permission reset, receipt bypass or safety relaxation.
|
|
12
|
+
- Release verification: bind mandatory phases to candidate bytes and fail on drift; run existing live-documentation checks before expensive gates and retain external per-run result metadata. Mandatory lanes, artifact budgets and timeouts are unchanged.
|
|
13
|
+
- Evidence limits: targeted regressions and independent reviews cover shared Claude/OMP sources and fixtures; Codex remains advisory where no native hook exists. Historical failed verifier runs remain failed. The review integration fixture now has a measured, first-case-only 15s allowance after bounded child-latency reproduction; global/runtime timeouts are unchanged. Discovery deadline testing waits for the intended filesystem phase instead of racing startup I/O. No speculative performance fix or exact historical slowdown attribution is claimed.
|
|
14
|
+
|
|
5
15
|
## 3.4.69 - 2026-10-25
|
|
6
16
|
|
|
7
17
|
**Narrow shared-lock cancellation repair; no new unattended-Handoff certification.**
|
|
@@ -4,6 +4,17 @@ description: >-
|
|
|
4
4
|
Machine-readable ownership/source registry for every documentation surface.
|
|
5
5
|
Hand-authored per SPEC §7; enforced by tests/consistency/docRegistry.test.js.
|
|
6
6
|
entries:
|
|
7
|
+
- id: systematic-loop-audit-2026-10-26
|
|
8
|
+
path: docs/analysis/SYSTEMATIC-LOOP-AUDIT-2026-10-26.md
|
|
9
|
+
class: canonical
|
|
10
|
+
audience: [maintainer, agent]
|
|
11
|
+
owner: release
|
|
12
|
+
source_of_truth: docs/analysis/SYSTEMATIC-LOOP-AUDIT-2026-10-26.md
|
|
13
|
+
merge_strategy: none
|
|
14
|
+
load_policy: on-demand
|
|
15
|
+
validation: [manual]
|
|
16
|
+
archive_policy: never
|
|
17
|
+
notes: bounded audit inventory and repair-round ledger; no additional full-verifier allowance
|
|
7
18
|
# ── Repo root ────────────────────────────────────────────────────────────
|
|
8
19
|
- id: root-claude-md
|
|
9
20
|
path: CLAUDE.md
|
|
@@ -451,6 +462,72 @@ entries:
|
|
|
451
462
|
validation: [manual]
|
|
452
463
|
archive_policy: replace-with-snapshot
|
|
453
464
|
notes: Phase A read-only concurrency audit; provisional metrics labelled, pilot not run
|
|
465
|
+
- id: docs-status-history-2026-10-25
|
|
466
|
+
path: docs/STATUS-HISTORY-2026-10-25.md
|
|
467
|
+
class: archive
|
|
468
|
+
audience: [maintainer]
|
|
469
|
+
owner: release
|
|
470
|
+
source_of_truth: docs/STATUS-HISTORY-2026-10-25.md
|
|
471
|
+
merge_strategy: none
|
|
472
|
+
load_policy: on-demand
|
|
473
|
+
validation: [manual]
|
|
474
|
+
archive_policy: never
|
|
475
|
+
notes: Superseded STATUS checkpoints moved verbatim from docs/STATUS.md (Round4)
|
|
476
|
+
- id: docs-analysis-round4-defect-closure
|
|
477
|
+
path: docs/analysis/ROUND4-DEFECT-CLOSURE.md
|
|
478
|
+
class: canonical
|
|
479
|
+
audience: [maintainer]
|
|
480
|
+
owner: release
|
|
481
|
+
source_of_truth: docs/analysis/ROUND4-DEFECT-CLOSURE.md
|
|
482
|
+
merge_strategy: none
|
|
483
|
+
load_policy: on-demand
|
|
484
|
+
validation: [manual]
|
|
485
|
+
archive_policy: never
|
|
486
|
+
notes: Round4 working defect-closure register; not an acceptance packet
|
|
487
|
+
- id: docs-analysis-round4-reviewer-failures
|
|
488
|
+
path: docs/analysis/ROUND4-REVIEWER-FAILURES.md
|
|
489
|
+
class: canonical
|
|
490
|
+
audience: [maintainer]
|
|
491
|
+
owner: release
|
|
492
|
+
source_of_truth: docs/analysis/ROUND4-REVIEWER-FAILURES.md
|
|
493
|
+
merge_strategy: none
|
|
494
|
+
load_policy: on-demand
|
|
495
|
+
validation: [manual]
|
|
496
|
+
archive_policy: never
|
|
497
|
+
notes: All 95 original reviewer failures with candidate-assessed dispositions; independent acceptance still required
|
|
498
|
+
- id: docs-analysis-round4-systemic-intent
|
|
499
|
+
path: docs/analysis/ROUND4-SYSTEMIC-INTENT.md
|
|
500
|
+
class: canonical
|
|
501
|
+
audience: [maintainer]
|
|
502
|
+
owner: release
|
|
503
|
+
source_of_truth: docs/analysis/ROUND4-SYSTEMIC-INTENT.md
|
|
504
|
+
merge_strategy: none
|
|
505
|
+
load_policy: on-demand
|
|
506
|
+
validation: [manual]
|
|
507
|
+
archive_policy: never
|
|
508
|
+
notes: Round4 systemic intent model and acceptance scope
|
|
509
|
+
- id: docs-analysis-round4-terminal-core-diagnostics
|
|
510
|
+
path: docs/analysis/ROUND4-TERMINAL-CORE-DIAGNOSTICS.md
|
|
511
|
+
class: archive
|
|
512
|
+
audience: [maintainer]
|
|
513
|
+
owner: release
|
|
514
|
+
source_of_truth: docs/analysis/ROUND4-TERMINAL-CORE-DIAGNOSTICS.md
|
|
515
|
+
merge_strategy: none
|
|
516
|
+
load_policy: on-demand
|
|
517
|
+
validation: [manual]
|
|
518
|
+
archive_policy: never
|
|
519
|
+
notes: Round4 terminal Core diagnostics evidence
|
|
520
|
+
- id: docs-debug-ukit-3431-debug-audit
|
|
521
|
+
path: docs/debug/UKIT-3.4.31-DEBUG-AUDIT.md
|
|
522
|
+
class: archive
|
|
523
|
+
audience: [maintainer]
|
|
524
|
+
owner: release
|
|
525
|
+
source_of_truth: docs/debug/UKIT-3.4.31-DEBUG-AUDIT.md
|
|
526
|
+
merge_strategy: none
|
|
527
|
+
load_policy: on-demand
|
|
528
|
+
validation: [manual]
|
|
529
|
+
archive_policy: never
|
|
530
|
+
notes: Unreleased 3.4.31 debug audit (2026-10-11); untracked file of unrecorded provenance, registered so the coverage gate sees it
|
|
454
531
|
- id: docs-analysis-concurrency-checkpoint
|
|
455
532
|
path: docs/analysis/CONCURRENCY-CHECKPOINT-2026-10-22.md
|
|
456
533
|
class: canonical
|
|
@@ -660,6 +737,17 @@ entries:
|
|
|
660
737
|
validation: [manual]
|
|
661
738
|
archive_policy: never
|
|
662
739
|
notes: Native planning Write permission evidence, authorized configuration provenance and unresolved live acceptance boundaries
|
|
740
|
+
- id: docs-analysis-handoff-create-git-completion
|
|
741
|
+
path: docs/analysis/HANDOFF-CREATE-GIT-COMPLETION.md
|
|
742
|
+
class: canonical
|
|
743
|
+
audience: [maintainer]
|
|
744
|
+
owner: product
|
|
745
|
+
source_of_truth: docs/analysis/HANDOFF-CREATE-GIT-COMPLETION.md
|
|
746
|
+
merge_strategy: none
|
|
747
|
+
load_policy: on-demand
|
|
748
|
+
validation: [manual]
|
|
749
|
+
archive_policy: never
|
|
750
|
+
notes: Create-scope Git authorization, operation evidence, task-owned denial reconciliation and release verification boundaries
|
|
663
751
|
- id: docs-analysis-orchestration-final-verification
|
|
664
752
|
path: docs/analysis/ORCHESTRATION-FINAL-VERIFICATION.md
|
|
665
753
|
class: archive
|
|
@@ -1535,7 +1535,8 @@ items:
|
|
|
1535
1535
|
type: config
|
|
1536
1536
|
sourceTemplate: .claude/ukit/runtime
|
|
1537
1537
|
targetPath: .claude/ukit/runtime
|
|
1538
|
-
requires:
|
|
1538
|
+
requires:
|
|
1539
|
+
- ukit-index-publication-intent-script
|
|
1539
1540
|
mergeStrategy: overwrite_with_backup
|
|
1540
1541
|
variables: []
|
|
1541
1542
|
enabledByDefault: true
|
|
@@ -1856,11 +1857,22 @@ items:
|
|
|
1856
1857
|
packs:
|
|
1857
1858
|
- core
|
|
1858
1859
|
|
|
1860
|
+
- id: ukit-index-publication-intent-script
|
|
1861
|
+
type: config
|
|
1862
|
+
sourceTemplate: .claude/ukit/index/publication-intent.mjs
|
|
1863
|
+
targetPath: .claude/ukit/index/publication-intent.mjs
|
|
1864
|
+
mergeStrategy: overwrite_with_backup
|
|
1865
|
+
variables: []
|
|
1866
|
+
enabledByDefault: true
|
|
1867
|
+
packs:
|
|
1868
|
+
- core
|
|
1869
|
+
|
|
1859
1870
|
- id: ukit-index-route-resolver-script
|
|
1860
1871
|
type: config
|
|
1861
1872
|
sourceTemplate: .claude/ukit/index/route-resolver.mjs
|
|
1862
1873
|
targetPath: .claude/ukit/index/route-resolver.mjs
|
|
1863
1874
|
requires:
|
|
1875
|
+
- ukit-index-publication-intent-script
|
|
1864
1876
|
- ukit-index-core-lib
|
|
1865
1877
|
- ukit-index-cache-utils-script
|
|
1866
1878
|
- ukit-index-route-catalog-script
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ngockhoale/ukit",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.70",
|
|
4
4
|
"description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex and omp (Oh My Pi).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
"bin/",
|
|
31
31
|
"src/",
|
|
32
32
|
"scripts/",
|
|
33
|
+
"!scripts/dev/benchmark-verification-currency.mjs",
|
|
34
|
+
"!scripts/dev/capture-omp-stall.mjs",
|
|
35
|
+
"!scripts/install/generate-semantic-engine.mjs",
|
|
36
|
+
"!scripts/release/progress-reporter.mjs",
|
|
33
37
|
"!scripts/audit/c100/attribution-gap.mjs",
|
|
34
38
|
"!scripts/audit/c100/implementer-attribution.mjs",
|
|
35
39
|
"!scripts/audit/c100/trees-lifetimes.mjs",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Static specifiers plus URL-resolved dynamic imports (`new URL('./x.mjs', import.meta.url)`), which runtimes use for
|
|
5
|
+
// lazy safety modules. Only literal module-file URLs are edges: assets (.json) and computed specifiers are not.
|
|
6
|
+
function relativeModuleDependencies(text) {
|
|
7
|
+
const imports = /(?:\b(?:import|export)\s+(?:[^;'"\n]*?\s+from\s*)?|\bimport\s*\(\s*)['"](\.{1,2}\/[^'"\n]+)['"]/g;
|
|
8
|
+
const urls = /\bnew\s+URL\(\s*['"](\.{1,2}\/[^'"\n]+\.[cm]?js)['"]\s*,\s*import\.meta\.url/g;
|
|
9
|
+
return [...new Set([...text.matchAll(imports), ...text.matchAll(urls)].map((match) => match[1]))];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function validateModuleClosureSync(entries) {
|
|
13
|
+
const targets = new Map(entries.filter((entry) => entry.sourcePath)
|
|
14
|
+
.map((entry) => [path.resolve(entry.targetPath), path.resolve(entry.sourcePath)]));
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
if (!entry.sourcePath || !/\.[cm]?js$/.test(entry.sourcePath)) continue;
|
|
17
|
+
const text = fs.readFileSync(entry.sourcePath, 'utf8');
|
|
18
|
+
for (const dependency of relativeModuleDependencies(text)) {
|
|
19
|
+
const source = path.resolve(path.dirname(entry.sourcePath), dependency);
|
|
20
|
+
const target = path.resolve(path.dirname(entry.targetPath), dependency);
|
|
21
|
+
if (targets.get(target) !== source) {
|
|
22
|
+
throw new Error(`Runtime dependency missing or misrelocated: ${entry.sourcePath} -> ${dependency} (expected ${target})`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Relocation preserves relative module topology. Missing declared dependencies
|
|
29
|
+
// are packaging errors, never an excuse to disable a runtime safety check.
|
|
30
|
+
export function copyModuleClosureSync(source, destination, visited = new Set()) {
|
|
31
|
+
source = path.resolve(source);
|
|
32
|
+
destination = path.resolve(destination);
|
|
33
|
+
const key = `${source}\0${destination}`;
|
|
34
|
+
if (visited.has(key)) return;
|
|
35
|
+
visited.add(key);
|
|
36
|
+
const bytes = fs.readFileSync(source);
|
|
37
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
38
|
+
fs.writeFileSync(destination, bytes);
|
|
39
|
+
if (!/\.[cm]?js$/.test(source)) return;
|
|
40
|
+
const text = bytes.toString('utf8');
|
|
41
|
+
for (const dependency of relativeModuleDependencies(text)) {
|
|
42
|
+
copyModuleClosureSync(path.resolve(path.dirname(source), dependency),
|
|
43
|
+
path.resolve(path.dirname(destination), dependency), visited);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function copyRuntimeClosureSync(sourceDirectory, destinationDirectory) {
|
|
48
|
+
const visited = new Set();
|
|
49
|
+
for (const entry of fs.readdirSync(sourceDirectory, { withFileTypes: true })) {
|
|
50
|
+
if (!entry.isFile()) continue;
|
|
51
|
+
copyModuleClosureSync(path.join(sourceDirectory, entry.name), path.join(destinationDirectory, entry.name), visited);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -140,6 +140,15 @@ async function collectMirrorOnlyPaths({ plan, projectRoot, templatesRoot }) {
|
|
|
140
140
|
return [...new Set(mirrorOnly)].sort();
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
// Hooks write their own state under `<managed dir>/.ukit/storage/` (stop-coordinator, task-watchdog,
|
|
144
|
+
// observability). That is protected runtime state, not template drift: it is reported for transparency but never
|
|
145
|
+
// fails the check, is never rewritten, and is never offered for deletion. Any other unshipped file stays mirror-only.
|
|
146
|
+
const RUNTIME_STATE_SEGMENT = '/.ukit/storage/';
|
|
147
|
+
function splitRuntimeState(paths) {
|
|
148
|
+
const runtimeState = paths.filter((p) => `/${p}`.includes(RUNTIME_STATE_SEGMENT));
|
|
149
|
+
return { mirrorOnly: paths.filter((p) => !runtimeState.includes(p)), runtimeState };
|
|
150
|
+
}
|
|
151
|
+
|
|
143
152
|
async function main() {
|
|
144
153
|
const args = process.argv.slice(2);
|
|
145
154
|
const unknown = args.filter((arg) => arg !== '--check');
|
|
@@ -192,11 +201,11 @@ async function main() {
|
|
|
192
201
|
.map((entry) => ({ entry, relativePath: toRelative(projectRoot, entry.targetPath) }))
|
|
193
202
|
.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
194
203
|
|
|
195
|
-
const mirrorOnly = await collectMirrorOnlyPaths({
|
|
204
|
+
const { mirrorOnly, runtimeState } = splitRuntimeState(await collectMirrorOnlyPaths({
|
|
196
205
|
plan,
|
|
197
206
|
projectRoot,
|
|
198
207
|
templatesRoot: pathConfig.templatesRoot,
|
|
199
|
-
});
|
|
208
|
+
}));
|
|
200
209
|
|
|
201
210
|
if (checkOnly) {
|
|
202
211
|
for (const { entry, relativePath } of drift) {
|
|
@@ -205,15 +214,19 @@ async function main() {
|
|
|
205
214
|
for (const relativePath of mirrorOnly) {
|
|
206
215
|
console.log(`MIRROR-ONLY ${relativePath}`);
|
|
207
216
|
}
|
|
217
|
+
for (const relativePath of runtimeState) {
|
|
218
|
+
console.log(`RUNTIME-STATE ${relativePath} (protected; not drift)`);
|
|
219
|
+
}
|
|
208
220
|
|
|
221
|
+
const stateNote = runtimeState.length ? `, ${runtimeState.length} protected runtime-state file(s) ignored` : '';
|
|
209
222
|
if (drift.length === 0 && mirrorOnly.length === 0) {
|
|
210
|
-
console.log(`ukit mirror check: clean (${managedRelativePaths.size} scoped targets)`);
|
|
223
|
+
console.log(`ukit mirror check: clean (${managedRelativePaths.size} scoped targets${stateNote})`);
|
|
211
224
|
return 0;
|
|
212
225
|
}
|
|
213
226
|
|
|
214
227
|
console.log(
|
|
215
228
|
`ukit mirror check: ${drift.length} drifted, ${mirrorOnly.length} mirror-only `
|
|
216
|
-
+ `(${managedRelativePaths.size} scoped targets)`,
|
|
229
|
+
+ `(${managedRelativePaths.size} scoped targets${stateNote})`,
|
|
217
230
|
);
|
|
218
231
|
return 1;
|
|
219
232
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { lstatSync, readFileSync, readlinkSync } from 'node:fs';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
// Runtime receipts are outputs of verification, not release candidate inputs.
|
|
7
|
+
// Canonical template_project runtime sources are deliberately NOT excluded.
|
|
8
|
+
const mutable = /^(?:\.ukit\/storage\/|\.claude\/ukit\/runtime\/\.ukit\/storage\/|\.cache\/|node_modules\/)/;
|
|
9
|
+
|
|
10
|
+
export function candidateFingerprint(root) {
|
|
11
|
+
const files = execFileSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard'], {
|
|
12
|
+
cwd: root, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, timeout: 30_000,
|
|
13
|
+
}).split('\0').filter((file) => file && !mutable.test(file));
|
|
14
|
+
const hash = createHash('sha256');
|
|
15
|
+
for (const file of [...new Set(files)].sort()) {
|
|
16
|
+
hash.update(JSON.stringify(file));
|
|
17
|
+
const absolute = path.join(root, file);
|
|
18
|
+
let stat;
|
|
19
|
+
try { stat = lstatSync(absolute); } catch (error) {
|
|
20
|
+
if (error.code !== 'ENOENT') throw error;
|
|
21
|
+
hash.update('\0deleted\0');
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
hash.update(`\0${stat.mode}\0`);
|
|
25
|
+
if (stat.isSymbolicLink()) hash.update(`link\0${readlinkSync(absolute)}`);
|
|
26
|
+
else if (stat.isFile()) hash.update(readFileSync(absolute));
|
|
27
|
+
else throw new Error(`Unsupported candidate entry: ${file}`);
|
|
28
|
+
hash.update('\0');
|
|
29
|
+
}
|
|
30
|
+
return hash.digest('hex');
|
|
31
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'node:fs';
|
|
1
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdtempSync } from 'node:fs';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { candidateFingerprint } from './candidate-state.mjs';
|
|
6
7
|
|
|
7
8
|
const rootDir = process.cwd();
|
|
8
9
|
const npmCacheDir = path.join(os.tmpdir(), 'ukit-npm-cache');
|
|
@@ -75,6 +76,12 @@ const steps = [
|
|
|
75
76
|
args: ['docs:render:check'],
|
|
76
77
|
env: process.env,
|
|
77
78
|
},
|
|
79
|
+
{
|
|
80
|
+
label: 'Live documentation preflight (headers, budgets, current status)',
|
|
81
|
+
command: 'yarn',
|
|
82
|
+
args: ['vitest', 'run', 'tests/consistency/liveStateDocs.test.js', 'tests/consistency/advisorStatusDocs.test.js'],
|
|
83
|
+
env: process.env,
|
|
84
|
+
},
|
|
78
85
|
{
|
|
79
86
|
label: 'Consistency suite (template<->active sync, watchdog coverage, render parity)',
|
|
80
87
|
command: 'yarn',
|
|
@@ -117,17 +124,46 @@ const steps = [
|
|
|
117
124
|
},
|
|
118
125
|
];
|
|
119
126
|
|
|
127
|
+
const runDirectory = mkdtempSync(path.join(os.tmpdir(), 'ukit-release-run-'));
|
|
128
|
+
const candidate = candidateFingerprint(rootDir);
|
|
129
|
+
const run = { pid: process.pid, candidate, startedAt: new Date().toISOString(), phase: null, result: 'running', exitCode: null };
|
|
130
|
+
const persistRun = () => writeFileSync(path.join(runDirectory, 'result.json'), `${JSON.stringify(run, null, 2)}\n`);
|
|
131
|
+
persistRun();
|
|
132
|
+
console.log(`[release:verify] Candidate ${candidate}; run evidence ${runDirectory}`);
|
|
133
|
+
process.on('exit', (code) => {
|
|
134
|
+
run.exitCode = code;
|
|
135
|
+
if (run.result === 'running') run.result = 'failed';
|
|
136
|
+
run.finishedAt = new Date().toISOString();
|
|
137
|
+
persistRun();
|
|
138
|
+
});
|
|
139
|
+
function assertCandidate() {
|
|
140
|
+
if (candidateFingerprint(rootDir) !== candidate) {
|
|
141
|
+
console.error('[release:verify] FAILED: candidate changed during verification; no PASS applies to the current bytes.');
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
120
146
|
for (const step of steps) {
|
|
147
|
+
assertCandidate();
|
|
148
|
+
run.phase = step.label;
|
|
149
|
+
run.phaseStartedAt = new Date().toISOString();
|
|
150
|
+
run.phaseResult = null;
|
|
151
|
+
step.env = { ...step.env, UKIT_VERIFY_PROGRESS_FILE: path.join(runDirectory, `phase-${steps.indexOf(step)}-progress.json`) };
|
|
152
|
+
persistRun();
|
|
121
153
|
console.log(`\n[release:verify] ${step.label}`);
|
|
122
154
|
console.log(`[release:verify] > ${step.command} ${step.args.join(' ')}`);
|
|
123
155
|
|
|
124
156
|
const exitCode = await runStep(step);
|
|
157
|
+
assertCandidate();
|
|
125
158
|
if (exitCode !== 0) {
|
|
126
159
|
console.error(`[release:verify] FAILED at step: ${step.label}`);
|
|
127
160
|
process.exit(exitCode ?? 1);
|
|
128
161
|
}
|
|
129
162
|
}
|
|
130
163
|
|
|
164
|
+
assertCandidate();
|
|
165
|
+
run.result = 'passed';
|
|
166
|
+
persistRun();
|
|
131
167
|
console.log('\n[release:verify] All release checks passed.');
|
|
132
168
|
|
|
133
169
|
// BUG-C22-20: bounded spawn — an armed watchdog SIGKILLs the child's whole
|
|
@@ -138,6 +174,16 @@ function spawnBounded(command, args, opts, timeoutMs) {
|
|
|
138
174
|
child.ukitTimedOut = false;
|
|
139
175
|
const watchdog = setTimeout(() => {
|
|
140
176
|
child.ukitTimedOut = true;
|
|
177
|
+
// One bounded native snapshot before termination, never a polling watcher.
|
|
178
|
+
// ps contains no argv/environment (credentials); unsupported hosts record the error.
|
|
179
|
+
if (!postPublish && opts.env?.UKIT_VERIFY_PROGRESS_FILE) try {
|
|
180
|
+
const snapshot = execFileSync('ps', ['-axo', 'pid,ppid,pgid,stat,etime,time,comm'], {
|
|
181
|
+
encoding: 'utf8', timeout: 1000, maxBuffer: 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'],
|
|
182
|
+
});
|
|
183
|
+
writeFileSync(path.join(runDirectory, 'deadline-processes.txt'), snapshot);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
writeFileSync(path.join(runDirectory, 'deadline-processes.txt'), `unavailable: ${error.code || error.message}\n`);
|
|
186
|
+
}
|
|
141
187
|
try {
|
|
142
188
|
process.kill(-child.pid, 'SIGKILL'); // kill the whole tree
|
|
143
189
|
} catch {
|
|
@@ -157,13 +203,21 @@ function runStep({ command, args, env }) {
|
|
|
157
203
|
stdio: 'inherit',
|
|
158
204
|
}, STEP_TIMEOUT_MS);
|
|
159
205
|
|
|
160
|
-
child.on('exit', (code) => {
|
|
206
|
+
child.on('exit', (code, signal) => {
|
|
207
|
+
run.phaseResult = { pid: child.pid, nativeExitCode: code, signal,
|
|
208
|
+
terminationReason: child.ukitTimedOut ? 'phase-deadline' : signal ? 'signal' : 'native-exit',
|
|
209
|
+
elapsedMs: Date.now() - Date.parse(run.phaseStartedAt) };
|
|
210
|
+
persistRun();
|
|
161
211
|
if (child.ukitTimedOut) {
|
|
162
|
-
console.error(`[release:verify] step timed out after ${STEP_TIMEOUT_MS}ms — child killed
|
|
212
|
+
console.error(`[release:verify] step timed out after ${STEP_TIMEOUT_MS}ms — child killed; phase progress: ${env.UKIT_VERIFY_PROGRESS_FILE}`);
|
|
163
213
|
}
|
|
164
|
-
resolve(code ?? 1);
|
|
214
|
+
resolve(child.ukitTimedOut ? 1 : (code ?? 1));
|
|
215
|
+
});
|
|
216
|
+
child.on('error', (error) => {
|
|
217
|
+
run.phaseResult = { pid: child.pid ?? null, nativeExitCode: null, terminationReason: 'spawn-error', error: error.message };
|
|
218
|
+
persistRun();
|
|
219
|
+
resolve(1);
|
|
165
220
|
});
|
|
166
|
-
child.on('error', () => resolve(1));
|
|
167
221
|
});
|
|
168
222
|
}
|
|
169
223
|
|
package/src/core/buildPlan.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { selectManifestItems } from '../manifest/selectItems.js';
|
|
4
4
|
import { renderTemplateString } from '../render/renderTemplate.js';
|
|
5
5
|
import { UKIT_PACKAGE_NAME } from './update.js';
|
|
6
|
+
import { validateModuleClosureSync } from '../../scripts/install/module-dependency-closure.mjs';
|
|
6
7
|
|
|
7
8
|
const BINARY_TEMPLATE_EXTENSIONS = new Set([
|
|
8
9
|
'.ttf',
|
|
@@ -325,6 +326,7 @@ export async function buildInstallPlan({
|
|
|
325
326
|
}
|
|
326
327
|
}
|
|
327
328
|
|
|
329
|
+
validateModuleClosureSync(planEntries);
|
|
328
330
|
await downgradeSelfInstallDocOverwrites(planEntries, { templatesRoot, projectRoot });
|
|
329
331
|
|
|
330
332
|
return {
|