@starci/hfs 5.0.4 → 5.0.6
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/README.md +22 -2
- package/cache/app-families.d.mts +8 -5
- package/cache/app-families.mjs +11 -9
- package/cache/hfs-family.d.mts +2 -0
- package/cache/hfs-family.mjs +4 -4
- package/cache/run.d.mts +2 -0
- package/cache/run.mjs +2 -5
- package/cache/session.mjs +1 -1
- package/cache/test-family.d.mts +6 -4
- package/cache/test-family.mjs +22 -14
- package/cache/types-family.d.mts +5 -3
- package/cache/types-family.mjs +9 -8
- package/cache/wiring-family.d.mts +4 -2
- package/cache/wiring-family.mjs +5 -5
- package/ci/cli.d.mts +7 -1
- package/ci/cli.mjs +37 -6
- package/ci/github.d.mts +1 -0
- package/ci/secret.d.mts +10 -1
- package/ci/secret.mjs +26 -6
- package/ci/verify.d.mts +9 -1
- package/ci/verify.mjs +5 -3
- package/emit/contracts.d.mts +12 -9
- package/emit/contracts.mjs +29 -11
- package/lint/chunk.mjs +11 -2
- package/lint/run.d.mts +2 -1
- package/lint/run.mjs +26 -29
- package/package.json +1 -1
- package/release/candidate-ci.mjs +1 -1
- package/release/gate.d.mts +1 -0
- package/release/gate.mjs +76 -18
- package/release/index.d.mts +1 -1
- package/release/index.mjs +7 -3
- package/runtime/knowledge/grammars/index.yaml +11 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +6 -6
- package/runtime/knowledge/hfs/rules.yaml +2 -2
- package/runtime/modules/kernel/failure-codes.yaml +4 -4
- package/runtime/modules/schemas/application-stacks.schema.yaml +10 -7
- package/runtime/scripts/api/node/run-node.d.mts +29 -0
- package/runtime/scripts/api/node/run-node.mjs +19 -0
- package/runtime/scripts/gates/starcistacks-ci.mjs +3 -1
- package/runtime/scripts/gates/starcistacks-form.d.mts +1 -0
- package/runtime/scripts/gates/starcistacks.d.mts +1 -0
- package/runtime/scripts/gates/starcistacks.mjs +22 -10
- package/runtime/scripts/hfs/app-wiring.d.mts +8 -2
- package/runtime/scripts/hfs/app-wiring.mjs +27 -7
- package/runtime/scripts/hfs/architecture/contract-bounds.d.mts +3 -0
- package/runtime/scripts/hfs/architecture/contract-bounds.mjs +72 -0
- package/runtime/scripts/hfs/architecture/index.mjs +1 -1
- package/runtime/scripts/hfs/architecture/tiers.d.mts +2 -1
- package/runtime/scripts/hfs/architecture/tiers.mjs +45 -4
- package/runtime/scripts/hfs/ci-declaration.d.mts +47 -1
- package/runtime/scripts/hfs/ci-declaration.mjs +115 -3
- package/runtime/scripts/hfs/repo-identity.d.mts +5 -1
- package/runtime/scripts/hfs/repo-identity.mjs +15 -10
- package/runtime/scripts/hfs/rules/ci-custody.mjs +21 -32
- package/runtime/scripts/hfs/rules/contract.d.mts +11 -3
- package/runtime/scripts/hfs/rules/contract.mjs +50 -11
- package/runtime/scripts/hfs/rules/env-parity.mjs +15 -6
- package/runtime/scripts/hfs/secret-standard.d.mts +1 -0
- package/runtime/scripts/hfs/secret-standard.mjs +25 -4
- package/runtime/scripts/hfs/secret-store.d.mts +5 -0
- package/runtime/scripts/hfs/secret-store.mjs +31 -0
- package/src/main.d.mts +3 -1
- package/src/main.mjs +14 -11
- package/sync/ci-commands.mjs +2 -1
- package/sync/quality-services.mjs +3 -2
- package/sync/readme-badges.mjs +1 -1
- package/sync/sonar-key.d.mts +2 -3
- package/sync/sonar-key.mjs +7 -14
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +7 -3
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +7 -3
package/release/gate.mjs
CHANGED
|
@@ -7,7 +7,9 @@ import { createHash } from 'node:crypto';
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { isPlainObject as plain } from '../runtime/engine/plain-object.mjs';
|
|
10
|
-
import { qualityServices } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
10
|
+
import { qualityServices, serviceBindings, projectServiceCredential, serviceCredentialNames } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
11
|
+
import { repositoryName, identityQueryEnvironment } from '../runtime/scripts/hfs/repo-identity.mjs';
|
|
12
|
+
import { serviceCredentialFile, serviceCredentialValue } from '../runtime/scripts/hfs/secret-store.mjs';
|
|
11
13
|
import { openHfs } from '../runtime/scripts/hfs/slots.mjs';
|
|
12
14
|
import { firstLine, lines, tool } from '../stack/context.mjs';
|
|
13
15
|
import { failure, refusal } from '../stack/errors.mjs';
|
|
@@ -74,21 +76,72 @@ function auditGate(audit) {
|
|
|
74
76
|
throw refusal(RELEASE_CODE, 'services.audit.level must be an npm audit level');
|
|
75
77
|
return { name: 'audit', command: ['npm', 'audit', ...(audit.omitDev ? ['--omit=dev'] : []), `--audit-level=${audit.level}`], summary: null, env: {} };
|
|
76
78
|
}
|
|
77
|
-
/**
|
|
78
|
-
function sonarGate(services,
|
|
79
|
+
/** The CI execution copy is job-local; a native owner run opens the selected product credential only when executed. */
|
|
80
|
+
function sonarGate(services, ctx, execute) {
|
|
81
|
+
const env = ctx.env;
|
|
79
82
|
const configured = services?.sonar;
|
|
80
|
-
|
|
83
|
+
let sonar = configured?.provider === 'sonarcloud' ? { ...configured, host: configured.host || 'https://sonarcloud.io' } : configured;
|
|
81
84
|
const tokenName = sonar?.token;
|
|
82
|
-
if (!sonar
|
|
83
|
-
throw refusal('RELEASE_SONAR_UNAVAILABLE', 'declare an enabled services.sonar host and
|
|
84
|
-
|
|
85
|
+
if (!sonar || !ENV_KEY.test(tokenName))
|
|
86
|
+
throw refusal('RELEASE_SONAR_UNAVAILABLE', 'declare an enabled services.sonar host and ci.token selector');
|
|
87
|
+
let token;
|
|
88
|
+
let privateHost;
|
|
89
|
+
if (env.GITHUB_ACTIONS === 'true') {
|
|
90
|
+
token = env[tokenName];
|
|
91
|
+
if (!token)
|
|
92
|
+
throw refusal('RELEASE_SONAR_UNAVAILABLE', 'the declared Sonar CI execution copy is missing');
|
|
93
|
+
if (sonar.hostCredential) {
|
|
94
|
+
privateHost = env[sonar.hostVar || 'SONAR_HOST_URL'];
|
|
95
|
+
sonar = { ...sonar, host: privateHost ?? null, privateHost: true };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const doc = services.declaration;
|
|
100
|
+
const repository = repositoryName(ctx.cwd, { env: identityQueryEnvironment(env, serviceCredentialNames(doc)) });
|
|
101
|
+
const selected = serviceBindings(doc, repository);
|
|
102
|
+
const binding = selected.bindings.find((item) => item.service === 'sonar' && item.name === tokenName);
|
|
103
|
+
if (selected.problems.length || !binding)
|
|
104
|
+
throw refusal('RELEASE_SONAR_UNAVAILABLE', 'declare one explicit Sonar project credential binding in this product');
|
|
105
|
+
try {
|
|
106
|
+
serviceCredentialFile(ctx.cwd, binding);
|
|
107
|
+
if (execute)
|
|
108
|
+
token = serviceCredentialValue(ctx.cwd, binding, ctx.sops);
|
|
109
|
+
if (sonar.hostCredential) {
|
|
110
|
+
const endpoint = projectServiceCredential(doc, 'sonar', sonar.hostCredential, repository);
|
|
111
|
+
if (!endpoint)
|
|
112
|
+
throw new Error('invalid endpoint reference');
|
|
113
|
+
serviceCredentialFile(ctx.cwd, endpoint);
|
|
114
|
+
if (execute)
|
|
115
|
+
privateHost = serviceCredentialValue(ctx.cwd, endpoint, ctx.sops);
|
|
116
|
+
sonar = { ...sonar, host: privateHost ?? null, privateHost: true };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw refusal('RELEASE_SONAR_UNAVAILABLE', 'the selected Sonar project custody is missing or cannot be opened');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!sonar.host && !(sonar.hostCredential && !execute))
|
|
124
|
+
throw refusal('RELEASE_SONAR_UNAVAILABLE', 'the declared Sonar connection URL is missing');
|
|
125
|
+
if (sonar.host) {
|
|
126
|
+
try {
|
|
127
|
+
const url = new URL(sonar.host);
|
|
128
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password)
|
|
129
|
+
throw new Error('invalid URL');
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
throw refusal('RELEASE_SONAR_UNAVAILABLE', 'the Sonar connection must be HTTP(S), with login kept in separate sealed credential keys');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const command = ['npx', '--yes', '@sonar/scan@4.3.0', ...(sonar.privateHost ? [] : [`-Dsonar.host.url=${sonar.host}`])];
|
|
136
|
+
if (sonar.projectKey)
|
|
137
|
+
command.push(`-Dsonar.projectKey=${sonar.projectKey}`);
|
|
85
138
|
const organization = sonar.organization ?? (sonar.organizationVar ? env[sonar.organizationVar] : null);
|
|
86
139
|
if (organization)
|
|
87
140
|
command.push(`-Dsonar.organization=${organization}`);
|
|
88
141
|
if (!command.every((word) => TOKEN.test(word)))
|
|
89
142
|
throw refusal('RELEASE_SONAR_UNAVAILABLE', 'Sonar host and organization must be plain command arguments');
|
|
90
|
-
return { name: 'sonar', command, summary: null, env: { SONAR_TOKEN:
|
|
91
|
-
credentialNames: [...new Set(['SONAR_TOKEN', tokenName])] };
|
|
143
|
+
return { name: 'sonar', command, summary: null, env: { ...(token === undefined ? {} : { SONAR_TOKEN: token }), ...(privateHost ? { SONAR_HOST_URL: privateHost } : {}) }, provider: sonar,
|
|
144
|
+
credentialNames: [...new Set(['SONAR_TOKEN', tokenName, ...serviceCredentialNames(services.declaration), ...(sonar.hostCredential ? ['SONAR_HOST_URL', sonar.hostVar] : [])].filter((name) => typeof name === 'string'))] };
|
|
92
145
|
}
|
|
93
146
|
function gateProblems(entry, at, seen) {
|
|
94
147
|
const found = [];
|
|
@@ -261,7 +314,7 @@ async function sonarAvailability(ctx, gate) {
|
|
|
261
314
|
}
|
|
262
315
|
if (up)
|
|
263
316
|
return null;
|
|
264
|
-
return { pass: false, detail: `${SONAR_UNREACHABLE}: ${sonar.host} did not report UP within ten seconds` };
|
|
317
|
+
return { pass: false, detail: `${SONAR_UNREACHABLE}: ${sonar.privateHost ? 'the declared private host' : sonar.host} did not report UP within ten seconds` };
|
|
265
318
|
}
|
|
266
319
|
/** An unreachable Sonar host stays a red row with a real log and no scan invocation. */
|
|
267
320
|
export async function runAvailableGate(ctx, gate, options) {
|
|
@@ -292,12 +345,18 @@ async function judgeQualityGate(ctx, gate, row) {
|
|
|
292
345
|
return row;
|
|
293
346
|
const started = ctx.now();
|
|
294
347
|
const verdict = await sonarQualityGate(ctx, { cwd: ctx.cwd, host: gate.provider.host, token: gate.env.SONAR_TOKEN });
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
348
|
+
const detail = redactGateText(gate, verdict.detail);
|
|
349
|
+
const failed = (verdict.conditions ?? []).map((line) => ` failed: ${redactGateText(gate, line)}\n`).join('');
|
|
350
|
+
fs.appendFileSync(row.log, `\nquality gate: ${detail}\n${failed}`);
|
|
351
|
+
return { ...row, pass: verdict.pass, detail, seconds: row.seconds + Math.round((ctx.now() - started) / 1000), logSha256: sha256(row.log) };
|
|
352
|
+
}
|
|
353
|
+
/** Scanner output and the later server verdict share one redaction boundary. */
|
|
354
|
+
function redactGateText(gate, text) {
|
|
355
|
+
const values = gate.provider ? [gate.env.SONAR_TOKEN, ...(gate.provider.privateHost ? [gate.env.SONAR_HOST_URL] : [])].filter((value) => Boolean(value)) : [];
|
|
356
|
+
return values.reduce((redacted, value) => redacted.replaceAll(value, '[REDACTED]'), text);
|
|
298
357
|
}
|
|
299
358
|
export const withoutCredentials = (values, credentialNames) => {
|
|
300
|
-
const selected = new Set(credentialNames);
|
|
359
|
+
const selected = new Set([...credentialNames, 'SOPS_AGE_KEY', 'SOPS_AGE_KEY_FILE'].map((name) => name.toUpperCase()));
|
|
301
360
|
return Object.fromEntries(Object.entries(values ?? {}).filter(([key]) => !selected.has(key.toUpperCase())));
|
|
302
361
|
};
|
|
303
362
|
export function runGate(ctx, gate, { dir, platform, credentialNames, preface = '' }) {
|
|
@@ -308,11 +367,10 @@ export function runGate(ctx, gate, { dir, platform, credentialNames, preface = '
|
|
|
308
367
|
const env = { ...ctx.env, ...(gate.provider ? gate.env : withoutCredentials(gate.env, credentialNames)), ...RELEASE_CACHE_ENV };
|
|
309
368
|
const run = tool(ctx, command, args, { cwd: ctx.cwd, env });
|
|
310
369
|
const output = `${run.stdout ?? ''}${run.stderr ?? ''}`;
|
|
311
|
-
const text =
|
|
370
|
+
const text = redactGateText(gate, output);
|
|
312
371
|
fs.writeFileSync(log, preface + text);
|
|
313
372
|
const verdict = judgeVerdict(run, gate, text);
|
|
314
|
-
|
|
315
|
-
verdict.detail = verdict.detail.replaceAll(gate.env.SONAR_TOKEN, '[REDACTED]');
|
|
373
|
+
verdict.detail = redactGateText(gate, verdict.detail);
|
|
316
374
|
const seconds = Math.round((ctx.now() - started) / 1000);
|
|
317
375
|
return {
|
|
318
376
|
name: gate.name, command: gate.command.join(' '), pass: verdict.pass, detail: verdict.detail, seconds,
|
|
@@ -459,7 +517,7 @@ function backfillRed(ctx, { common, urgent, failed, openTicket }) {
|
|
|
459
517
|
export async function releaseGate(ctx, config, options = {}) {
|
|
460
518
|
// Sonar is a row of the managed CI, so it is a row of the local gate whenever the app declares services.sonar; a missing token is a refusal, never a skip.
|
|
461
519
|
if (config.services?.sonar)
|
|
462
|
-
config = { ...config, gates: [...config.gates.filter((gate) => gate.name !== 'sonar'), sonarGate(config.services, ctx.
|
|
520
|
+
config = { ...config, gates: [...config.gates.filter((gate) => gate.name !== 'sonar'), sonarGate(config.services, ctx, options.execute === true)] };
|
|
463
521
|
const credentialNames = config.gates.find(gate => gate.provider)?.credentialNames ?? ['SONAR_TOKEN', config.services?.sonar?.token].filter(Boolean);
|
|
464
522
|
ctx = Object.create(ctx, { env: { value: withoutCredentials(ctx.env, credentialNames) } });
|
|
465
523
|
// The CI verdict of a release is the candidate run of its own sha, judged by `starci app release push` (candidate-ci.mjs, OWNER RULING candidate-ci 2026-10-18):
|
package/release/index.d.mts
CHANGED
|
@@ -42,13 +42,13 @@ export declare function contextOf(io?: VerbIo): VerbContext;
|
|
|
42
42
|
export declare function readDeclaration(root: string): Record<string, any>;
|
|
43
43
|
/** The normalised `release:` block (tolerant of absence) with the package version: { root, version, gates, tagPrefix, defaultBranch, requireCi }. */
|
|
44
44
|
export declare function loadReleaseConfig(root: string): {
|
|
45
|
+
services: any;
|
|
45
46
|
root: string;
|
|
46
47
|
version: any;
|
|
47
48
|
gates: import("./gate.mjs").Gate[];
|
|
48
49
|
tagPrefix: string;
|
|
49
50
|
defaultBranch: string;
|
|
50
51
|
requireCi: boolean;
|
|
51
|
-
services: any;
|
|
52
52
|
};
|
|
53
53
|
/** Parses the flags, runs `run(ctx, flags, positional)` and prints its result; returns the exit code. Only StackError is mapped, anything else is a bug and propagates. */
|
|
54
54
|
export declare function runVerb({ label, name, io, argv, spec, run, code }: {
|
package/release/index.mjs
CHANGED
|
@@ -10,11 +10,13 @@ import path from 'node:path';
|
|
|
10
10
|
import { isPlainObject as plain } from '../runtime/engine/plain-object.mjs';
|
|
11
11
|
import { parseYaml } from '../runtime/engine/yaml.mjs';
|
|
12
12
|
import { isRuntimeExampleApp } from '../runtime/scripts/lib/runtime-example.mjs';
|
|
13
|
+
import { repositoryName, identityQueryEnvironment } from '../runtime/scripts/hfs/repo-identity.mjs';
|
|
14
|
+
import { serviceCredentialNames } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
13
15
|
import { hasEntityList } from '../db/entity-list.mjs';
|
|
14
16
|
import { STACKS_DIR, STACKS_FILE } from '../stack/config.mjs';
|
|
15
17
|
import { stackContext } from '../stack/context.mjs';
|
|
16
18
|
import { EXIT, StackError, refusal } from '../stack/errors.mjs';
|
|
17
|
-
import { RELEASE_CODE, VERSION, normalizeRelease, releaseGate } from './gate.mjs';
|
|
19
|
+
import { RELEASE_CODE, VERSION, normalizeRelease, releaseGate, withoutCredentials } from './gate.mjs';
|
|
18
20
|
import { releasePush } from './push.mjs';
|
|
19
21
|
import { releaseUrgentPush } from './urgent.mjs';
|
|
20
22
|
const IO_KEYS = ['cwd', 'stdout', 'stderr', 'env', 'sops', 'hostIdentity'];
|
|
@@ -77,10 +79,11 @@ export function loadReleaseConfig(root) {
|
|
|
77
79
|
if (typeof pkg.version !== 'string' || !VERSION.test(pkg.version))
|
|
78
80
|
throw refusal(RELEASE_CODE, 'package.json needs a version like 1.2.3 (the release tag derives from it)');
|
|
79
81
|
const declaration = readDeclaration(root);
|
|
80
|
-
const { config, problems } = normalizeRelease(declaration.release, pkg.scripts, declaration.services, { example: isRuntimeExampleApp(root), entities: hasEntityList(root)
|
|
82
|
+
const { config, problems } = normalizeRelease(declaration.release, pkg.scripts, declaration.services, { example: isRuntimeExampleApp(root), entities: hasEntityList(root),
|
|
83
|
+
repository: repositoryName(root, { env: identityQueryEnvironment(process.env, serviceCredentialNames(declaration)) }) });
|
|
81
84
|
if (!config)
|
|
82
85
|
throw refusal(RELEASE_CODE, `${problems.length} problem(s) in release: ${problems.slice(0, 6).join('; ')}`);
|
|
83
|
-
return { root, version: pkg.version, ...config };
|
|
86
|
+
return { root, version: pkg.version, ...config, services: { ...config.services, declaration } };
|
|
84
87
|
}
|
|
85
88
|
function print(ctx, name, flags, result) {
|
|
86
89
|
if (flags.json)
|
|
@@ -119,6 +122,7 @@ const ACTIONS = {
|
|
|
119
122
|
if (flags.reason !== undefined && flags.urgent !== true)
|
|
120
123
|
throw refusal('RELEASE_USAGE', '--reason belongs to an urgent release (--urgent)');
|
|
121
124
|
const config = loadReleaseConfig(ctx.cwd);
|
|
125
|
+
ctx = Object.create(ctx, { env: { value: withoutCredentials(ctx.env, serviceCredentialNames(config.services?.declaration)) } });
|
|
122
126
|
if (flags.urgent === true)
|
|
123
127
|
return releaseUrgentPush(ctx, config, { execute: flags.execute === true, remote: flags.remote, reason: flags.reason, role: harness?.role ?? ctx.env.STARCI_ROLE });
|
|
124
128
|
return releasePush(ctx, config, { execute: flags.execute === true, remote: flags.remote });
|
|
@@ -69,7 +69,17 @@ families:
|
|
|
69
69
|
entry: '@starci/grammar/common'
|
|
70
70
|
styleEntry: '@starci/grammar/common.css'
|
|
71
71
|
semanticRoles:
|
|
72
|
-
page:
|
|
72
|
+
page:
|
|
73
|
+
export: Page
|
|
74
|
+
entries: ['@starci/grammar/common', '@starci/grammar/<family>']
|
|
75
|
+
segment: { attribute: header, member: tabs }
|
|
76
|
+
bodyContainers: [PaneGrid, Pane, Stack, PaneSection, Section]
|
|
77
|
+
# Native body transparency follows the renderer's exact context, status and intrinsic-element API.
|
|
78
|
+
bodyContainerConstraints:
|
|
79
|
+
Pane: { providesContext: pane }
|
|
80
|
+
Stack: { literalProps: { as: [div, header, footer] } }
|
|
81
|
+
PaneSection: { requiresContext: pane, absentProps: [error], literalProps: { as: [div, header, footer] } }
|
|
82
|
+
Section: { absentProps: [error], literalProps: { kind: [region] } }
|
|
73
83
|
peers: [react, '@heroui/react', '@xyflow/react', react-aria-components]
|
|
74
84
|
packageSnapshot: common/DNA.yaml
|
|
75
85
|
scope: |
|
|
@@ -31,35 +31,35 @@ pins:
|
|
|
31
31
|
source: packages/event-stream/package.json
|
|
32
32
|
why: 'the one server-sent-events client of every front end (fe-stack event-stream, owner:event-stream-package): the transport client layer (modules/api/sse/clients) creates one instance for the app with the bearer token source and the stream operations follow on it; it carries Last-Event-ID resume, the capped doubling backoff, the heartbeat timeout, the single 401 renewal and the useSWRSubscription adapter, so no app keeps a hand-written reader.'
|
|
33
33
|
'@starci/grammar':
|
|
34
|
-
version: 0.21.
|
|
34
|
+
version: 0.21.10
|
|
35
35
|
group: starci
|
|
36
36
|
install: registry
|
|
37
37
|
side: fe
|
|
38
38
|
source: packages/grammar/package.json
|
|
39
39
|
why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
|
|
40
40
|
'@starci/eslint-canon-be':
|
|
41
|
-
version: 3.11.
|
|
41
|
+
version: 3.11.17
|
|
42
42
|
group: starci
|
|
43
43
|
install: registry
|
|
44
44
|
side: be
|
|
45
45
|
source: packages/eslint-canon-be/package.json
|
|
46
46
|
why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
|
|
47
47
|
'@starci/eslint-canon-fe':
|
|
48
|
-
version: 9.9.
|
|
48
|
+
version: 9.9.10
|
|
49
49
|
group: starci
|
|
50
50
|
install: registry
|
|
51
51
|
side: fe
|
|
52
52
|
source: packages/eslint-canon-fe/package.json
|
|
53
53
|
why: 'the front-end ESLint canon: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; the front end has no tests (FE_NO_TESTS R97 in hfs) and no-vietnamese-in-source (R91) applies.'
|
|
54
54
|
'@starci/eslint-grammar-core':
|
|
55
|
-
version: 0.21.
|
|
55
|
+
version: 0.21.10
|
|
56
56
|
group: starci
|
|
57
57
|
install: registry
|
|
58
58
|
side: fe
|
|
59
59
|
source: packages/eslint-grammar-core/package.json
|
|
60
60
|
why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
|
|
61
61
|
'@starci/eslint-grammar-offset-pop':
|
|
62
|
-
version: 0.21.
|
|
62
|
+
version: 0.21.10
|
|
63
63
|
group: starci
|
|
64
64
|
install: registry
|
|
65
65
|
side: fe
|
|
@@ -106,7 +106,7 @@ pins:
|
|
|
106
106
|
source: packages/ai-router/package.json
|
|
107
107
|
why: 'the AI pool router of a back end: a NestJS module that routes OpenAI-compatible chat, stream and embeddings calls across keys, models and providers, with @starci/ai-router/testing for its specs; @starci/jest-preset compiles it as an ESM-only dependency.'
|
|
108
108
|
'@starci/hfs':
|
|
109
|
-
version: 5.0.
|
|
109
|
+
version: 5.0.6
|
|
110
110
|
group: starci
|
|
111
111
|
install: registry
|
|
112
112
|
side: both
|
|
@@ -5107,7 +5107,7 @@ rules:
|
|
|
5107
5107
|
- {path: packages/eslint/be/module-shape.spec.mjs, editions: [lite, full], codes: [BE_MODULE_DEFINITION_MISSING]}
|
|
5108
5108
|
- id: R296
|
|
5109
5109
|
code: "CI_SECRET_SEALED"
|
|
5110
|
-
law: "A
|
|
5110
|
+
law: "A sealed CI credential copy names an explicit service credential bound to the selected product project, environment, member and key; required product Sonar analysis uses sonar.enc.yaml and an app runtime key never becomes a CI credential. Checks inspect names without decrypting or printing values."
|
|
5111
5111
|
kinds: [check]
|
|
5112
5112
|
gates: [settle, land, ci]
|
|
5113
5113
|
failureCodes: ["CI_SECRET_SEALED"]
|
|
@@ -5129,7 +5129,7 @@ rules:
|
|
|
5129
5129
|
- {path: packages/hfs/src/ci-custody.spec.mjs, editions: [lite, full], codes: [CI_SECRET_SEALED]}
|
|
5130
5130
|
- id: R297
|
|
5131
5131
|
code: "APP_SECRET_IN_CI"
|
|
5132
|
-
law: "A sealed member key
|
|
5132
|
+
law: "A sealed member key selected by an app runtime secret declaration or deploy grant never appears in ci.secrets or ci.vars. The consumer environment, member and selected key determine app custody; online GitHub custody is verified by starci app ci verify."
|
|
5133
5133
|
kinds: [check]
|
|
5134
5134
|
gates: [settle, land, ci]
|
|
5135
5135
|
failureCodes: ["APP_SECRET_IN_CI"]
|
|
@@ -1742,12 +1742,12 @@ CI_DECLARATION_DRIFT:
|
|
|
1742
1742
|
kind: check-finding
|
|
1743
1743
|
|
|
1744
1744
|
CI_SECRET_SEALED:
|
|
1745
|
-
title: "CI
|
|
1745
|
+
title: "CI credential lacks exact project service custody"
|
|
1746
1746
|
title_vi: "Tên CI trùng khóa bí mật đã niêm phong"
|
|
1747
|
-
meaning_vi: "
|
|
1747
|
+
meaning_vi: "Bản sao khóa CI không trỏ đúng dịch vụ, dự án, môi trường, thành viên và khóa đã niêm phong; khóa chạy ứng dụng không thuộc CI."
|
|
1748
1748
|
causes_vi:
|
|
1749
|
-
- "
|
|
1750
|
-
nextStep_vi: "
|
|
1749
|
+
- "Thiếu liên kết credential của dịch vụ, sai dự án hoặc dùng khóa được cấp cho container ứng dụng."
|
|
1750
|
+
nextStep_vi: "Giữ khóa runtime ngoài CI; khai báo đúng credential.project và liên kết services.<id>.ci.secrets cho bản sao khóa công cụ."
|
|
1751
1751
|
owner: op-retry
|
|
1752
1752
|
kind: check-finding
|
|
1753
1753
|
|
|
@@ -196,7 +196,7 @@ $defs:
|
|
|
196
196
|
local: {type: string, pattern: '^https?://[^\s]+$'}
|
|
197
197
|
public: {type: string, pattern: '^https://[^\s]+$'}
|
|
198
198
|
fromCredential:
|
|
199
|
-
description: A
|
|
199
|
+
description: A private connection URL carried inside a sealed credential - its credential id, never the value. Public local or hosted URLs use local or public.
|
|
200
200
|
type: string
|
|
201
201
|
pattern: '^[a-z][a-z0-9-]*$'
|
|
202
202
|
stack: {$ref: '#/$defs/serviceStack'}
|
|
@@ -432,17 +432,19 @@ $defs:
|
|
|
432
432
|
publishedBy: {type: string, minLength: 1}
|
|
433
433
|
serviceCredential:
|
|
434
434
|
description: >-
|
|
435
|
-
A
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
435
|
+
A name-only connection or credential reference: its repository, actual sealed member path and key.
|
|
436
|
+
env names the consuming tool's variable. project references projects[].key for project-scoped tooling;
|
|
437
|
+
ids identify references independently of provider project-key syntax. Username, password and private
|
|
438
|
+
endpoint keys use separate references; operator/admin login lives in a separate authority member and
|
|
439
|
+
is never an implicit scanner or CI credential. Product Sonar analysis names exactly
|
|
440
|
+
.starcistacks/<declared-env>/secrets/sonar.enc.yaml; ci-declaration.mjs enforces that service-specific member.
|
|
441
|
+
Legacy plaintext paths with .enc twins require migration.
|
|
441
442
|
type: object
|
|
442
443
|
additionalProperties: false
|
|
443
444
|
required: [id, custody]
|
|
444
445
|
properties:
|
|
445
446
|
id: {type: string, pattern: '^[a-z][a-z0-9-]*$'}
|
|
447
|
+
project: {type: string, minLength: 1, description: "The exact projects[].key this credential serves; execution selects the entry by the actual consumer repository."}
|
|
446
448
|
purpose: {type: string, minLength: 1}
|
|
447
449
|
env: {type: string, pattern: '^[A-Z][A-Z0-9_]*$'}
|
|
448
450
|
key: {type: string, minLength: 1}
|
|
@@ -469,6 +471,7 @@ $defs:
|
|
|
469
471
|
workflow: {type: string, minLength: 1}
|
|
470
472
|
permissions: {type: array, items: {type: string, minLength: 1}}
|
|
471
473
|
secrets:
|
|
474
|
+
description: Explicit credential-to-CI execution-copy links; names are declared in the top-level ci block. ci.token selects the analysis copy.
|
|
472
475
|
type: array
|
|
473
476
|
items:
|
|
474
477
|
type: object
|
|
@@ -1,3 +1,32 @@
|
|
|
1
1
|
import type { SpawnSyncOptions, SpawnSyncReturns } from 'node:child_process';
|
|
2
|
+
/** A concrete Node child whose admission belongs to the CLI's machine-wide governor. */
|
|
3
|
+
export interface HeavyNodeRequest {
|
|
4
|
+
readonly kind: 'tsc' | 'eslint' | 'test';
|
|
5
|
+
readonly label: string;
|
|
6
|
+
readonly args: readonly string[];
|
|
7
|
+
readonly cwd: string;
|
|
8
|
+
/** When supplied, this is the entire child environment, not an overlay. */
|
|
9
|
+
readonly env?: NodeJS.ProcessEnv | undefined;
|
|
10
|
+
readonly timeoutMs?: number | undefined;
|
|
11
|
+
readonly maxBuffer: number;
|
|
12
|
+
}
|
|
13
|
+
/** Normal red exits are measurements; an interrupted child is not one. */
|
|
14
|
+
export interface HeavyNodeResult {
|
|
15
|
+
readonly status: number | null;
|
|
16
|
+
readonly signal: NodeJS.Signals | null;
|
|
17
|
+
readonly stdout: string;
|
|
18
|
+
readonly stderr: string;
|
|
19
|
+
readonly error?: NodeJS.ErrnoException | undefined;
|
|
20
|
+
}
|
|
21
|
+
/** The CLI admits and captures each actual child, never the composite app command. */
|
|
22
|
+
export type HeavyNodeRun = (request: HeavyNodeRequest) => Promise<HeavyNodeResult>;
|
|
23
|
+
/** A real governor hold reached no verdict; transports preserve its CLI exit instead of treating it as linter output. */
|
|
24
|
+
export declare const isHostHeld: (error: unknown) => boolean;
|
|
25
|
+
/** Execution infrastructure reached no complete measurement; the contract finder cannot turn it into a cacheable source finding. */
|
|
26
|
+
export declare const isNodeExecutionRefusal: (error: unknown) => boolean;
|
|
27
|
+
/** Keeps the runtime's mutation fence and temporary-directory custody before crossing the CLI execution port. */
|
|
28
|
+
export declare function runHeavyNode(run: HeavyNodeRun | undefined, request: HeavyNodeRequest): Promise<HeavyNodeResult>;
|
|
29
|
+
/** A partial compiler/test report cannot be interpreted or stored as a completed measurement. */
|
|
30
|
+
export declare function completedNode(result: HeavyNodeResult, label: string): HeavyNodeResult;
|
|
2
31
|
/** Runs `node <args>`; options (cwd, env, timeout, input, maxBuffer, stdio) pass through. */
|
|
3
32
|
export declare const runNode: (args: readonly string[], options?: SpawnSyncOptions) => SpawnSyncReturns<string>;
|
|
@@ -1,3 +1,22 @@
|
|
|
1
1
|
import { nodeSpawn } from './lib.mjs';
|
|
2
|
+
import { assertMutationFence } from '../../lib/mutation-fence.mjs';
|
|
3
|
+
import { withTempEnv } from '../../../engine/temp-root.mjs';
|
|
4
|
+
/** A real governor hold reached no verdict; transports preserve its CLI exit instead of treating it as linter output. */
|
|
5
|
+
export const isHostHeld = (error) => error instanceof Error && 'code' in error && error.code === 'HOST_HELD' && 'exitCode' in error && error.exitCode === 75;
|
|
6
|
+
/** Execution infrastructure reached no complete measurement; the contract finder cannot turn it into a cacheable source finding. */
|
|
7
|
+
export const isNodeExecutionRefusal = (error) => error instanceof Error && 'code' in error && typeof error.code === 'string' && ['HOST_HELD', 'HEAVY_DEFERRED', 'CLI_TOOL_INCOMPLETE', 'CLI_TOOL_MISSING', 'CLI_TOOL_NOT_STARTED', 'HFS_NODE_GOVERNOR_MISSING', 'HFS_NODE_INCOMPLETE'].includes(error.code);
|
|
8
|
+
/** Keeps the runtime's mutation fence and temporary-directory custody before crossing the CLI execution port. */
|
|
9
|
+
export async function runHeavyNode(run, request) {
|
|
10
|
+
if (run === undefined)
|
|
11
|
+
throw Object.assign(new Error('HFS_NODE_GOVERNOR_MISSING: run this check through starci-harness'), { code: 'HFS_NODE_GOVERNOR_MISSING' });
|
|
12
|
+
assertMutationFence({ kind: 'node-effect', args: request.args });
|
|
13
|
+
return run({ ...request, env: withTempEnv({ env: request.env ?? process.env }).env });
|
|
14
|
+
}
|
|
15
|
+
/** A partial compiler/test report cannot be interpreted or stored as a completed measurement. */
|
|
16
|
+
export function completedNode(result, label) {
|
|
17
|
+
if (result.error !== undefined || result.signal !== null || result.status === null)
|
|
18
|
+
throw Object.assign(new Error(`${label} did not complete`), { code: 'HFS_NODE_INCOMPLETE' });
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
2
21
|
/** Runs `node <args>`; options (cwd, env, timeout, input, maxBuffer, stdio) pass through. */
|
|
3
22
|
export const runNode = (args, options = {}) => nodeSpawn(args, options);
|
|
@@ -22,7 +22,7 @@ export const catalogOf = (id) => {
|
|
|
22
22
|
export const checkServiceForm = (service, id, entry, at, kind, add, ambiguous) => {
|
|
23
23
|
if (service.provider && !catalogOf(id).providers.includes(service.provider))
|
|
24
24
|
add('refuse', 'STACKS_PROVIDER_UNKNOWN', `${at}.provider`, `${service.provider} is not a ${id} provider (${catalogOf(id).providers.join(', ')})`);
|
|
25
|
-
if (Object.hasOwn(entry?.ci ?? {}, '
|
|
25
|
+
if (Object.hasOwn(entry?.ci ?? {}, 'vars') || service.ci.secrets.some((item) => !item.credential))
|
|
26
26
|
add('suspect', 'STACKS_CI_LEGACY_FORM', `${at}.ci`, 'move the names to the top-level ci block and name them by services.<id>.ci.token / hostVar');
|
|
27
27
|
if (id === 'sonar' && service.provider === 'sonarcloud' && !service.organization
|
|
28
28
|
&& !(kind === 'example' && service.runtimeSecrets.length && !service.credentials.length && service.ci.organizationVar))
|
|
@@ -56,6 +56,8 @@ export function checkQualityService(service, id, at, context, checkServiceCi) {
|
|
|
56
56
|
ambiguous('codecov auth must be oidc or token');
|
|
57
57
|
if (id === 'codecov' && service.auth === 'token' && !service.ci.token)
|
|
58
58
|
ambiguous('codecov auth token requires ci.token');
|
|
59
|
+
if (id === 'codecov' && service.auth === 'oidc' && (service.ci.token || service.credentials.length || service.ci.secrets.length))
|
|
60
|
+
ambiguous('codecov OIDC has no token, credential or sealed member');
|
|
59
61
|
checkServiceCi(service, id, at, context);
|
|
60
62
|
return true;
|
|
61
63
|
}
|
|
@@ -42,6 +42,8 @@ import { readProperties } from '../lib/properties.mjs';
|
|
|
42
42
|
import { escapeRegExp } from '../lib/regex.mjs';
|
|
43
43
|
import { isInfrastructureValueFile } from './starcistacks-value-file.mjs';
|
|
44
44
|
import { STACK_DECLARATION_TEMPLATE } from '../lib/example-refs.mjs';
|
|
45
|
+
import { isSealedName } from '../lib/sealed-member.mjs';
|
|
46
|
+
import { serviceBindings } from '../hfs/ci-declaration.mjs';
|
|
45
47
|
import { checkSecretForm, kindOf } from './starcistacks-form.mjs';
|
|
46
48
|
import { SERVICE_CATALOG, catalogOf, checkServiceForm, checkCiDeclaration, checkQualityService } from './starcistacks-ci.mjs';
|
|
47
49
|
import { applicationStackFindings } from './starcistacks-runtime.mjs';
|
|
@@ -165,9 +167,10 @@ const normalizeCredential = (credential, declaringRepo) => {
|
|
|
165
167
|
const repo = resolveDeclaredRepository(custody.repository, { fromRepo: declaringRepo });
|
|
166
168
|
const rel = text(custody.path);
|
|
167
169
|
const abs = resolveCustodyFile(repo, rel);
|
|
168
|
-
|
|
170
|
+
const sealed = rel && isSealedName(rel) ? abs : abs ? `${abs}.enc` : null;
|
|
171
|
+
return { id: text(credential.id), env: text(credential.env), key: text(credential.key), purpose: text(credential.purpose), project: text(credential.project),
|
|
169
172
|
custody: { repository: text(custody.repository), path: rel, resolved: Boolean(repo), file: abs,
|
|
170
|
-
encPresent:
|
|
173
|
+
encPresent: sealed ? isFile(sealed) : null, plainPresent: abs && !isSealedName(abs) ? isFile(abs) : false } };
|
|
171
174
|
};
|
|
172
175
|
const ownerActionOf = (s) => {
|
|
173
176
|
const noAction = s.ownerAction === 'none' ? 'none' : null;
|
|
@@ -239,12 +242,9 @@ export function resolveStackService(repoRoot, serviceId) {
|
|
|
239
242
|
const listed = service.projects.some((project) => project.repository === name);
|
|
240
243
|
if (declaration.sourceHost && !listed)
|
|
241
244
|
continue;
|
|
242
|
-
//
|
|
243
|
-
// token (a credential whose id is a project key); other projects' tokens and the host's CI wiring are not its.
|
|
244
|
-
const ownKey = service.projects.find((project) => project.repository === name)?.key;
|
|
245
|
-
const projectKeys = new Set(service.projects.map((project) => project.key));
|
|
245
|
+
// A source host lends public server settings only; analysis credentials stay in the selected product's custody.
|
|
246
246
|
if (declaration.sourceHost)
|
|
247
|
-
service = { ...service, credentials:
|
|
247
|
+
service = { ...service, credentials: [],
|
|
248
248
|
ci: { wiring: null, workflow: null, permissions: [], secrets: [], vars: [], provisioning: null } };
|
|
249
249
|
return { declaration: declaration.file, declaringRepo: declaration.repo, sourceHost: Boolean(declaration.sourceHost),
|
|
250
250
|
repository: name, projectKey: service.projects.find((project) => project.repository === name)?.key ?? null, ...service };
|
|
@@ -304,7 +304,7 @@ const checkServiceCredentials = (service, at, add, ambiguous) => {
|
|
|
304
304
|
if (!credential.custody.resolved)
|
|
305
305
|
add('suspect', 'STACKS_CUSTODY_UNVERIFIED', where, `custody repository ${credential.custody.repository} is not checked out on this machine`);
|
|
306
306
|
else if (!credential.custody.encPresent)
|
|
307
|
-
add('refuse', 'STACKS_CUSTODY_MISSING', where, `${credential.custody.repository}/${credential.custody.path}
|
|
307
|
+
add('refuse', 'STACKS_CUSTODY_MISSING', where, `${credential.custody.repository}/${credential.custody.path} is not sealed in custody; repair the declared member through starci app secret`);
|
|
308
308
|
}
|
|
309
309
|
return ids;
|
|
310
310
|
};
|
|
@@ -371,6 +371,18 @@ const checkService = (id, entry, { schema, declFile, declaration, governing, nam
|
|
|
371
371
|
checkServiceForm(service, id, entry, at, kind, add, ambiguous);
|
|
372
372
|
if (checkDisabledService(service, id, at, ciUses, ambiguous, add))
|
|
373
373
|
return;
|
|
374
|
+
let product = false;
|
|
375
|
+
try {
|
|
376
|
+
product = JSON.parse(fs.readFileSync(path.join(declaration?.repo ?? '', 'hfs.json'), 'utf8')).kind === 'app';
|
|
377
|
+
}
|
|
378
|
+
catch { /* other checks own a missing app declaration */ }
|
|
379
|
+
if (id === 'sonar' && product) {
|
|
380
|
+
const selected = serviceBindings(declaration?.doc, name);
|
|
381
|
+
for (const problem of selected.problems.filter((problem) => problem.at.startsWith('services.sonar')))
|
|
382
|
+
ambiguous(problem.message);
|
|
383
|
+
if (service.ci.wiring === 'required' && !selected.bindings.some((binding) => binding.service === 'sonar' && binding.name === service.ci.token))
|
|
384
|
+
ambiguous('required Sonar analysis needs this product project credential in its declared environment sonar.enc.yaml member');
|
|
385
|
+
}
|
|
374
386
|
if (checkQualityService(service, id, at, { governing, name, ciUses, ciText, add, ambiguous }, checkServiceCi))
|
|
375
387
|
return;
|
|
376
388
|
checkEnabledService(service, id, at, { governing, name, ciUses, ciText, add, kind }, ambiguous);
|
|
@@ -546,7 +558,7 @@ export function checkStarciStacks(repoRoot, { newRepo = false, examplesRoot = pa
|
|
|
546
558
|
function summary(service) {
|
|
547
559
|
return { provider: service.provider, mode: service.mode, auth: service.auth, host: service.host,
|
|
548
560
|
stack: service.stack ? { owner: service.stack.owner ?? null, repository: service.stack.repository, root: service.stack.root, environment: service.stack.environment, compose: service.stack.compose, container: service.stack.container } : null,
|
|
549
|
-
projects: service.projects, credentials: service.credentials.map((credential) => ({ id: credential.id, env: credential.env, key: credential.key,
|
|
561
|
+
projects: service.projects, credentials: service.credentials.map((credential) => ({ id: credential.id, env: credential.env, key: credential.key, project: credential.project,
|
|
550
562
|
custody: `${credential.custody.repository}/${credential.custody.path}`, present: credential.custody.encPresent ?? null })),
|
|
551
563
|
ci: service.ci, ownerAction: service.ownerAction };
|
|
552
564
|
}
|
|
@@ -575,7 +587,7 @@ export function ownerAskConflict({ repo, question } = {}) {
|
|
|
575
587
|
continue;
|
|
576
588
|
const custody = service.credentials.map((credential) => `${credential.custody.repository}/${credential.custody.path}`);
|
|
577
589
|
const custodyNote = custody.length ? ` and its credentials in custody (${custody.join(', ')})` : ` and auth ${service.auth}`;
|
|
578
|
-
const sonarNote = id === 'sonar' ? '
|
|
590
|
+
const sonarNote = id === 'sonar' ? 'The native app release gate selects this product project credential; analysis never provisions projects or rotates credentials. ' : '';
|
|
579
591
|
const provisioningNote = service.ci.provisioning ? ` (${service.ci.provisioning})` : '';
|
|
580
592
|
return { service: id, provider: service.provider, declaration: slash(service.declaration), matched: byName ?? byWord,
|
|
581
593
|
message: `${id} (${service.provider}, ${service.mode}) is declared in ${slash(service.declaration)} with ownerAction none${custodyNote}; the owner is never asked for it. ${sonarNote}CI wiring is ${service.ci.wiring ?? 'undeclared'}${provisioningNote}.` };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type HeavyNodeRun, type HeavyNodeResult } from '../api/node/run-node.mjs';
|
|
1
2
|
export declare const BOOT_PROOF_CODE = "BE_APP_BOOT_PROOF_MISSING";
|
|
2
3
|
export declare const BOOT_PROOF_TIMEOUT = "BOOT_PROOF_TIMEOUT";
|
|
3
4
|
export declare const BOOT_PROOF_TIMEOUT_ENV = "STARCI_BOOT_PROOF_TIMEOUT_MS";
|
|
@@ -6,7 +7,7 @@ export declare const wiringFixturePath: (app: string) => string;
|
|
|
6
7
|
export type WorkerResult = {
|
|
7
8
|
status?: number | null;
|
|
8
9
|
signal?: string | null;
|
|
9
|
-
error?:
|
|
10
|
+
error?: HeavyNodeResult['error'];
|
|
10
11
|
stdout?: string;
|
|
11
12
|
} | null | undefined;
|
|
12
13
|
/** The injectable spawn seam of the worker. */
|
|
@@ -62,7 +63,12 @@ export interface AppWiringInput {
|
|
|
62
63
|
timeout?: number;
|
|
63
64
|
run?: WorkerRun;
|
|
64
65
|
}
|
|
65
|
-
|
|
66
|
+
/** The synchronous finder and its existing worker doubles drive the same proof program as app check. */
|
|
67
|
+
export declare function appWiring(input: AppWiringInput): WiringResult;
|
|
68
|
+
/** The cache's asynchronous execution seam admits each boot attempt, with the same validation, timeout retry and result judgement. */
|
|
69
|
+
export declare function appWiringAsync(input: Omit<AppWiringInput, 'run'> & {
|
|
70
|
+
execute?: HeavyNodeRun | undefined;
|
|
71
|
+
}): Promise<WiringResult>;
|
|
66
72
|
/** Optional standalone entry; coordinator connects the same seam to app check --gate wiring. */
|
|
67
73
|
export interface AppWiringMainIo {
|
|
68
74
|
cwd?: string;
|