@starci/hfs 4.8.0 → 4.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/emit/operation-schemas.mjs +3 -0
- package/lint/bound-sys.cjs +2 -2
- package/package.json +1 -1
- package/runtime/engine/admission.mjs +1 -1
- package/runtime/engine/config.mjs +2 -2
- package/runtime/engine/db/machine.mjs +2 -3
- package/runtime/engine/invalid-config.mjs +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -10
- package/runtime/knowledge/hfs/rules.yaml +23 -0
- package/runtime/modules/kernel/failure-codes.yaml +10 -0
- package/runtime/scripts/guards/git-policy.mjs +2 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +5 -5
- package/runtime/scripts/lib/package-at.mjs +5 -6
- package/runtime/scripts/lib/roots.mjs +2 -2
- package/runtime/scripts/work/draw/draw-dna-findings.mjs +23 -12
- package/runtime/scripts/work/draw/draw-dna.mjs +14 -8
- package/scaffold/app.mjs +4 -4
- package/src/main.mjs +3 -3
- package/sync/index.mjs +14 -1
- package/sync/managed.mjs +3 -2
- package/templates/app/codegen-graphql.mjs.partial +4 -4
- package/templates/app/quality-config/sonar-project.properties +4 -2
- package/runtime/engine/canonical-json.mjs +0 -14
- package/runtime/engine/db/ledger-open.mjs +0 -38
- package/runtime/engine/db/ledger-paths.mjs +0 -87
- package/runtime/engine/db/ledger.mjs +0 -1068
- package/runtime/engine/db/provision-authority.mjs +0 -76
- package/runtime/engine/db/provision-operations.mjs +0 -146
- package/runtime/packages/grammar/scripts/build-stamp.mjs +0 -98
- package/runtime/scripts/agent/codex-app-server.mjs +0 -79
- package/runtime/scripts/agent/codex-mirror-source.mjs +0 -34
- package/runtime/scripts/agent/host-agents.mjs +0 -80
- package/runtime/scripts/agent/launch-trust-policy.mjs +0 -53
- package/runtime/scripts/agent/trust-launch.mjs +0 -191
- package/runtime/scripts/agent/trust.mjs +0 -523
- package/runtime/scripts/api/fs/temp-path.mjs +0 -8
- package/runtime/scripts/api/git/diff-names.mjs +0 -8
- package/runtime/scripts/api/git/head-time.mjs +0 -10
- package/runtime/scripts/api/git/index-lock.mjs +0 -78
- package/runtime/scripts/api/git/log.mjs +0 -5
- package/runtime/scripts/api/git/merge-tree.mjs +0 -5
- package/runtime/scripts/api/git/rev-list.mjs +0 -5
- package/runtime/scripts/api/git/rev-parse.mjs +0 -13
- package/runtime/scripts/api/git/status-query.mjs +0 -5
- package/runtime/scripts/api/git/worktree-list-query.mjs +0 -5
- package/runtime/scripts/api/node/spawn-node.mjs +0 -6
- package/runtime/scripts/api/npm/dist-tag.mjs +0 -8
- package/runtime/scripts/api/npm/lib.mjs +0 -32
- package/runtime/scripts/api/npm/pack-dry-run.mjs +0 -9
- package/runtime/scripts/api/npm/run-npm.mjs +0 -6
- package/runtime/scripts/api/npm/view.mjs +0 -30
- package/runtime/scripts/api/npm/whoami.mjs +0 -8
- package/runtime/scripts/api/process/hide-child-windows.mjs +0 -31
- package/runtime/scripts/api/process/kill-tree.mjs +0 -22
- package/runtime/scripts/api/process/lib.mjs +0 -125
- package/runtime/scripts/api/process/probe-guard-command.mjs +0 -74
- package/runtime/scripts/api/process/process-list.mjs +0 -28
- package/runtime/scripts/api/process/system-tool.mjs +0 -37
- package/runtime/scripts/gates/canon-digest.mjs +0 -81
- package/runtime/scripts/gates/canon-pins.mjs +0 -119
- package/runtime/scripts/gates/gate-input.mjs +0 -98
- package/runtime/scripts/gates/gate.mjs +0 -636
- package/runtime/scripts/gates/grammar-dist.mjs +0 -206
- package/runtime/scripts/gates/output.mjs +0 -40
- package/runtime/scripts/gates/runtime-package-selection.mjs +0 -27
- package/runtime/scripts/gates/test-world-run.mjs +0 -252
- package/runtime/scripts/gates/type-impact.mjs +0 -277
- package/runtime/scripts/gates/type-program.mjs +0 -113
- package/runtime/scripts/guards/assigned-command.mjs +0 -12
- package/runtime/scripts/guards/call-rights.mjs +0 -47
- package/runtime/scripts/guards/command-guard.mjs +0 -468
- package/runtime/scripts/guards/command-policy.mjs +0 -342
- package/runtime/scripts/guards/critic-reach.mjs +0 -82
- package/runtime/scripts/guards/deps-guard.mjs +0 -150
- package/runtime/scripts/guards/env-dump-verdict.mjs +0 -89
- package/runtime/scripts/guards/guards-root.mjs +0 -27
- package/runtime/scripts/guards/hook-install.mjs +0 -343
- package/runtime/scripts/guards/hook-verdict.mjs +0 -12
- package/runtime/scripts/guards/install-verdict.mjs +0 -41
- package/runtime/scripts/guards/kernel-seat.mjs +0 -63
- package/runtime/scripts/guards/launch-verdict.mjs +0 -74
- package/runtime/scripts/guards/op-starci.mjs +0 -16
- package/runtime/scripts/guards/op-write-scope.mjs +0 -27
- package/runtime/scripts/guards/orca-self-lifecycle.mjs +0 -39
- package/runtime/scripts/guards/process-kill-verdict.mjs +0 -106
- package/runtime/scripts/guards/protected-zone.mjs +0 -56
- package/runtime/scripts/guards/refusals.mjs +0 -27
- package/runtime/scripts/guards/rights.mjs +0 -268
- package/runtime/scripts/guards/seat-tools.mjs +0 -47
- package/runtime/scripts/guards/shell-commands.mjs +0 -328
- package/runtime/scripts/guards/supervisor-seat.mjs +0 -80
- package/runtime/scripts/guards/verdict-process.mjs +0 -81
- package/runtime/scripts/lib/app-data.mjs +0 -11
- package/runtime/scripts/lib/canon-binding-text.mjs +0 -129
- package/runtime/scripts/lib/check-format.mjs +0 -7
- package/runtime/scripts/lib/failure-code-reads.mjs +0 -100
- package/runtime/scripts/lib/failure-code-template-errors.mjs +0 -161
- package/runtime/scripts/lib/git-dir.mjs +0 -120
- package/runtime/scripts/lib/guard-command.mjs +0 -24
- package/runtime/scripts/lib/interrupt-cleanup.mjs +0 -38
- package/runtime/scripts/lib/json-formats.mjs +0 -34
- package/runtime/scripts/lib/managed-json.mjs +0 -51
- package/runtime/scripts/lib/menu-parts.mjs +0 -11
- package/runtime/scripts/lib/op-gate.mjs +0 -15
- package/runtime/scripts/lib/provision-control-failure.mjs +0 -32
- package/runtime/scripts/lib/release-version.mjs +0 -134
- package/runtime/scripts/lib/runtime-location.mjs +0 -21
- package/runtime/scripts/lib/runtime-package-inventory.mjs +0 -21
- package/runtime/scripts/lib/runtime-package-manifests.mjs +0 -84
- package/runtime/scripts/lib/runtime-publication-tags.mjs +0 -56
- package/runtime/scripts/lib/seat-env.mjs +0 -43
- package/runtime/scripts/lib/sonar-new-code.mjs +0 -58
- package/runtime/scripts/lib/source-package-imports.mjs +0 -111
- package/runtime/scripts/lib/tar-files.mjs +0 -39
- package/runtime/scripts/lib/uat-run-folder.mjs +0 -118
- package/runtime/scripts/lib/worktree-record.mjs +0 -14
- package/runtime/scripts/machine/ask-receipts.mjs +0 -113
- package/runtime/scripts/machine/evidence-store.mjs +0 -223
- package/runtime/scripts/machine/home.mjs +0 -216
- package/runtime/scripts/machine/host-lock.mjs +0 -184
- package/runtime/scripts/machine/lock-recovery.mjs +0 -55
- package/runtime/scripts/machine/roles-contract.mjs +0 -90
- package/runtime/scripts/machine/runtime-change.mjs +0 -20
- package/runtime/scripts/machine/workflow-tree.mjs +0 -122
- package/runtime/scripts/reconciler/checklist-items.mjs +0 -7
- package/runtime/scripts/reconciler/guard-row.mjs +0 -25
- package/runtime/scripts/work/brand/brand-colour.mjs +0 -125
- package/runtime/scripts/work/brand/brand-tokens.mjs +0 -167
- package/runtime/scripts/work/brand/brand-value-source.mjs +0 -106
- package/runtime/scripts/work/brand/brand.mjs +0 -892
- package/runtime/scripts/work/draw/draw-layer.mjs +0 -306
- package/runtime/scripts/work/draw/draw-rationale-page.mjs +0 -237
- package/runtime/scripts/work/draw/draw-rationale-ref.mjs +0 -14
- package/runtime/scripts/work/draw/draw-rationale.mjs +0 -353
- package/runtime/scripts/work/draw/draw-source.mjs +0 -412
- package/runtime/scripts/work/draw/draw-taste.mjs +0 -278
- package/runtime/scripts/work/draw/draw-typecheck.mjs +0 -72
- package/runtime/scripts/work/draw/rationale-findings.mjs +0 -205
- package/runtime/scripts/work/draw-grammar.mjs +0 -175
- package/runtime/scripts/work/draw-render-page.mjs +0 -188
- package/runtime/scripts/work/draw-render.mjs +0 -626
- package/runtime/scripts/work/png.mjs +0 -237
- package/runtime/scripts/work/render-tools.mjs +0 -64
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// managed-json.mjs - which JSON files are managed canon rather than authored JSON (knowledge/hfs/slots.yaml `managedBy` /
|
|
2
|
-
// `liteManagedBy`): a template of @starci/hfs that a managed slot names (templates/<scope or common>/<group>/<path, each leading dot
|
|
3
|
-
// dropped>), declared by the slot manifest and nowhere else, and an app's render of one, which `starci app sync` writes. Only a template
|
|
4
|
-
// with no `{{variable}}` is a render candidate, and only a file whose text equals that template (line endings aside) is its render:
|
|
5
|
-
// a file that differs in any byte stays authored.
|
|
6
|
-
import { braceVariants } from './glob.mjs';
|
|
7
|
-
|
|
8
|
-
const SIDES = Object.freeze(['be', 'fe']);
|
|
9
|
-
const lf = (text) => String(text).replace(/\r\n/g, '\n');
|
|
10
|
-
/** A literal file path of a slot (no wildcard or variable): only those are whole managed files. */
|
|
11
|
-
const literal = (file) => !/[*?[<]/.test(file);
|
|
12
|
-
const undotted = (file) => file.split('/').map((segment) => segment.replace(/^\./, '')).join('/');
|
|
13
|
-
|
|
14
|
-
/** The scope (app, be or fe) and scope-relative path of an app-relative path, one per profile a slot may hold it in. */
|
|
15
|
-
const placementsOf = (appRel) => [
|
|
16
|
-
{ scope: 'app', file: appRel },
|
|
17
|
-
...SIDES.filter((side) => appRel.startsWith(`${side}/`)).map((side) => ({ scope: side, file: appRel.slice(side.length + 1) })),
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* The template names (relative to the @starci/hfs templates folder) a managed slot of `manifest` renders `appRel` from, in every
|
|
22
|
-
* edition: [] when no managed slot lists that literal file.
|
|
23
|
-
*/
|
|
24
|
-
export function managedTemplatesOf(manifest, appRel) {
|
|
25
|
-
const placements = placementsOf(appRel);
|
|
26
|
-
const matches = managedFilesOf(manifest).filter((managed) => placements.some(({ scope, file }) => managed.scope === scope && managed.file === file));
|
|
27
|
-
return [...new Set(matches.map((managed) => managed.name))];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Every literal file of every managed slot, with its scope and the template name it is rendered from: [{scope, file, name}]. */
|
|
31
|
-
function managedFilesOf(manifest) {
|
|
32
|
-
return (manifest?.slots ?? []).flatMap((slot) => {
|
|
33
|
-
const groups = [...new Set([slot.managedBy, slot.liteManagedBy].filter((group) => typeof group === 'string'))];
|
|
34
|
-
const paths = [slot.path, slot.lite?.path].filter((value) => typeof value === 'string').flatMap((value) => braceVariants(value)).filter(literal);
|
|
35
|
-
return (slot.profiles ?? []).flatMap((scope) => paths.flatMap((file) => groups.flatMap((group) =>
|
|
36
|
-
[scope, 'common'].map((dir) => ({ scope, file, name: `${dir}/${group}/${undotted(file)}` })))));
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Every template name a managed slot of `manifest` declares (its literal files in each profile, edition group and scope or common
|
|
42
|
-
* folder): the canon source `starci app sync` renders from, which the slot manifest itself names, so a template is declared there and
|
|
43
|
-
* nowhere else.
|
|
44
|
-
*/
|
|
45
|
-
export function declaredTemplateNames(manifest) {
|
|
46
|
-
return new Set(managedFilesOf(manifest).map((managed) => managed.name));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Whether `text` is the render of one of `templates` ([{name, text}]): a template with no variable whose text equals it. */
|
|
50
|
-
export const isManagedRender = (text, templates) =>
|
|
51
|
-
templates.some((template) => typeof template.text === 'string' && !template.text.includes('{{') && lf(template.text) === lf(text));
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// The parts the Kernel menu and the Supervisor menu share: a template filled from an item's subject, the escape option every
|
|
2
|
-
// item carries, and the plain words of a starci call a seat table is read by.
|
|
3
|
-
|
|
4
|
-
/** `template` with each `{key}` replaced by the subject's value; a key the subject does not know stays as written. */
|
|
5
|
-
export const fillTemplate = (template, subject) => String(template ?? '').replace(/\{(\w+)\}/g, (match, key) => (subject[key] == null ? match : String(subject[key])));
|
|
6
|
-
|
|
7
|
-
/** The escape option of a menu catalog (`escape: {choice, effect}`): no step, a reason as its text. */
|
|
8
|
-
export const escapeOptionOf = (escape, extra = {}) => ({ choice: escape.choice, ...extra, steps: [], effect: escape.effect, text: 'reason', escape: true });
|
|
9
|
-
|
|
10
|
-
/** The first `count` words of an argument list that are not options. */
|
|
11
|
-
export const plainWords = (args, count) => args.filter((value) => !String(value).startsWith('-')).slice(0, count).map(String);
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// op-gate.mjs — one section of the runtime's knowledge/op-gate.yaml (testWorld, unitKit, ...):
|
|
2
|
-
// {required, forbidden, outage, ...} as authored. An absent section reads as {}.
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { parseYaml } from '../../engine/yaml.mjs';
|
|
6
|
-
|
|
7
|
-
/** The `section` object of knowledge/op-gate.yaml under `runtime`, {} when the doc omits it. Throws when the file is unreadable. */
|
|
8
|
-
const opGateSection = (runtime, section) =>
|
|
9
|
-
(parseYaml(fs.readFileSync(path.join(runtime, 'knowledge', 'op-gate.yaml'), 'utf8'))?.[section]) ?? {};
|
|
10
|
-
|
|
11
|
-
/** The {required, forbidden, outage} rule lists of a gate `section` (absent lists read as []). */
|
|
12
|
-
export const opGateRules = (runtime, section) => {
|
|
13
|
-
const rules = opGateSection(runtime, section);
|
|
14
|
-
return { required: rules.required ?? [], forbidden: rules.forbidden ?? [], outage: rules.outage ?? [] };
|
|
15
|
-
};
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
// provision-control-failure.mjs - why `starci provision <plan|show|accept>` could not read or accept a prepared operation, as one public code and one
|
|
2
|
-
// value-free detail line. Pure: the caller (scripts/gates/provision-operation.mjs runProvisionControl) names the stage that failed and passes the error;
|
|
3
|
-
// this never serializes an error message, which may quote a provider body or a value, only the error's own identifier.
|
|
4
|
-
// The case that motivated it: an operation prepared by a lane runtime keeps its plan blob in THAT runtime's blob store, and the published CLI reading it
|
|
5
|
-
// from another runtime found the ledger row but not the blob; a blanket `provision-control-unavailable` hid that.
|
|
6
|
-
|
|
7
|
-
/** The stages of the control verb, in the order it runs them. */
|
|
8
|
-
export const PROVISION_CONTROL_STAGES = Object.freeze(['ledger', 'plan', 'target', 'accept']);
|
|
9
|
-
|
|
10
|
-
const IDENTIFIER = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/u;
|
|
11
|
-
const SHA = /^[a-f0-9]{64}$/u;
|
|
12
|
-
|
|
13
|
-
/** The error's own code (ERR_SQLITE_ERROR, ENOENT) or class name, never its message; null when it has neither. */
|
|
14
|
-
export function errorIdentifier(error) {
|
|
15
|
-
return [error?.code, error?.name].find((value) => typeof value === 'string' && IDENTIFIER.test(value)) ?? null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* {code, detail} for a failure of the control verb at `stage`. A plan blob the ledger names but this runtime's blob store does not hold is
|
|
20
|
-
* `provision-plan-blob-missing`; every other failure stays `provision-control-unavailable` with the stage and the error's identifier.
|
|
21
|
-
*/
|
|
22
|
-
export function provisionControlFailure(error, { stage, planSha = null, operationId = null } = {}) {
|
|
23
|
-
if (!PROVISION_CONTROL_STAGES.includes(stage)) throw new TypeError(`unknown provision control stage: ${String(stage)}`);
|
|
24
|
-
const id = errorIdentifier(error);
|
|
25
|
-
if (stage === 'plan' && id === 'ENOENT') {
|
|
26
|
-
const blob = typeof planSha === 'string' && SHA.test(planSha) ? `the plan blob ${planSha.slice(0, 12)}` : 'the plan blob';
|
|
27
|
-
const operation = typeof operationId === 'string' && IDENTIFIER.test(operationId) ? ` of operation ${operationId}` : '';
|
|
28
|
-
return { code: 'provision-plan-blob-missing', detail: `${blob}${operation} is not in this runtime's blob store: the operation was prepared by another runtime; run the control with the runtime that prepared it, or prepare the operation again with this one` };
|
|
29
|
-
}
|
|
30
|
-
const named = id ? ' (' + id + ')' : '';
|
|
31
|
-
return { code: 'provision-control-unavailable', detail: `the ${stage} stage failed${named}; the operation journal holds its state` };
|
|
32
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
// release-version.mjs - the one reading of a release version (owner ruling release-waves). A prerelease `<label>.N` opens the release
|
|
2
|
-
// line N and `<label>.N.M` is the M-th release after it, a wave or a fix alike, so alpha.31 < alpha.31.1 < alpha.31.2 < alpha.32 in
|
|
3
|
-
// SemVer order. The next release is derived from the highest one already released, never guessed. The legacy `<label>.N-fix.K` still
|
|
4
|
-
// reads as release K after `<label>.N` and is never produced again. Pure: the callers read the tags and the registry.
|
|
5
|
-
|
|
6
|
-
const NUMERIC = /^(?:0|[1-9]\d*)$/;
|
|
7
|
-
const POSITIVE = /^[1-9]\d*$/;
|
|
8
|
-
const IDENTIFIERS = /^[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*$/;
|
|
9
|
-
const LEGACY_FIX = '-fix.';
|
|
10
|
-
|
|
11
|
-
/** {base, wave, legacy}: the release a version counts its waves from, and its wave M (0 for the release that opens the line). */
|
|
12
|
-
export function waveOf(version) {
|
|
13
|
-
const text = String(version ?? '').split('+')[0];
|
|
14
|
-
const fix = text.lastIndexOf(LEGACY_FIX);
|
|
15
|
-
const legacyWave = fix > 0 ? text.slice(fix + LEGACY_FIX.length) : '';
|
|
16
|
-
if (POSITIVE.test(legacyWave)) return { base: text.slice(0, fix), wave: Number(legacyWave), legacy: true };
|
|
17
|
-
const ids = text.split('.');
|
|
18
|
-
const counted = ids.length > 3 && POSITIVE.test(ids.at(-1)) && (text.includes('-') ? NUMERIC.test(ids.at(-2)) : ids.length === 4);
|
|
19
|
-
return counted ? { base: ids.slice(0, -1).join('.'), wave: Number(ids.at(-1)), legacy: false } : { base: text, wave: 0, legacy: false };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Parsed SemVer identifiers {core, pre} of `version`; an unreadable version throws. */
|
|
23
|
-
export function versionParts(version) {
|
|
24
|
-
const parts = typeof version === 'string' ? version.split('+') : [];
|
|
25
|
-
if (!parts.length || parts.length > 2 || (parts.length === 2 && !IDENTIFIERS.test(parts[1]))) throw new Error(`unreadable release version ${version}`);
|
|
26
|
-
const boundary = parts[0].indexOf('-');
|
|
27
|
-
const core = (boundary < 0 ? parts[0] : parts[0].slice(0, boundary)).split('.');
|
|
28
|
-
const prerelease = boundary < 0 ? null : parts[0].slice(boundary + 1);
|
|
29
|
-
if (core.length !== 3 || core.some((part) => !NUMERIC.test(part)) || (prerelease !== null && !IDENTIFIERS.test(prerelease))) throw new Error(`unreadable release version ${version}`);
|
|
30
|
-
const pre = prerelease?.split('.') ?? [];
|
|
31
|
-
if (pre.some((part) => /^0\d+$/.test(part))) throw new Error(`invalid numeric prerelease identifier in ${version}`);
|
|
32
|
-
return { core: core.map(BigInt), pre };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const readable = (version) => {
|
|
36
|
-
try {
|
|
37
|
-
versionParts(version);
|
|
38
|
-
return true;
|
|
39
|
-
} catch {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/** The SemVer spelling a version is ordered by: a legacy `-fix.K` orders as wave K of its line. */
|
|
45
|
-
const ordered = (version) => {
|
|
46
|
-
const { base, wave, legacy } = waveOf(version);
|
|
47
|
-
return legacy ? `${base}.${wave}` : version;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const compareIdentifier = (a, b) => {
|
|
51
|
-
const numericA = /^\d+$/.test(a), numericB = /^\d+$/.test(b);
|
|
52
|
-
if (numericA && numericB) return BigInt(a) < BigInt(b) ? -1 : Number(BigInt(a) > BigInt(b));
|
|
53
|
-
if (numericA !== numericB) return numericA ? -1 : 1;
|
|
54
|
-
return a < b ? -1 : Number(a > b);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/** SemVer precedence, numeric prerelease identifiers included; build metadata has no precedence. */
|
|
58
|
-
export function compareVersions(a, b) {
|
|
59
|
-
const left = versionParts(ordered(a)), right = versionParts(ordered(b));
|
|
60
|
-
for (const i of left.core.keys()) {
|
|
61
|
-
if (left.core[i] !== right.core[i]) return left.core[i] < right.core[i] ? -1 : 1;
|
|
62
|
-
}
|
|
63
|
-
if (!left.pre.length || !right.pre.length) return Number(!left.pre.length) - Number(!right.pre.length);
|
|
64
|
-
for (let i = 0; i < Math.max(left.pre.length, right.pre.length); i += 1) {
|
|
65
|
-
if (left.pre[i] === undefined) return -1;
|
|
66
|
-
if (right.pre[i] === undefined) return 1;
|
|
67
|
-
const compared = compareIdentifier(left.pre[i], right.pre[i]);
|
|
68
|
-
if (compared) return compared;
|
|
69
|
-
}
|
|
70
|
-
return 0;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** The release line of `version`: a prerelease's core and the identifiers before its first number (every alpha.N and alpha.N.M share one), a stable release's major and minor. */
|
|
74
|
-
export function releaseLine(version) {
|
|
75
|
-
const { core, pre } = versionParts(ordered(version));
|
|
76
|
-
if (!pre.length) return `${core.slice(0, 2).join('.')}-stable`;
|
|
77
|
-
const first = pre.findIndex((id) => /^\d+$/.test(id));
|
|
78
|
-
return `${core.join('.')}-${(first < 0 ? pre : pre.slice(0, first)).join('.')}`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** {family, line} of a release that opens a line (`<x.y.z>-<label>.N`), or null for any other shape. */
|
|
82
|
-
function lineStart(base) {
|
|
83
|
-
if (!readable(base)) return null;
|
|
84
|
-
const { core, pre } = versionParts(base);
|
|
85
|
-
return pre.length === 2 && !/^\d+$/.test(pre[0]) && NUMERIC.test(pre[1]) ? { family: `${core.join('.')}-${pre[0]}`, line: Number(pre[1]) } : null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** The next wave after `version`: its base with M one past the highest wave of that base among `version` and `known`. */
|
|
89
|
-
export function nextWave(version, known = []) {
|
|
90
|
-
const { base } = waveOf(version);
|
|
91
|
-
const waves = [version, ...known].map(waveOf).filter((entry) => entry.base === base).map((entry) => entry.wave);
|
|
92
|
-
return `${base}.${Math.max(0, ...waves) + 1}`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** The family of a version: its core and label up to `-<label>` (`1.0.0-alpha`), or null for a stable or unlabelled version. */
|
|
96
|
-
export function familyOf(version) {
|
|
97
|
-
if (!readable(version)) return null;
|
|
98
|
-
const { core, pre } = versionParts(ordered(version));
|
|
99
|
-
return pre.length && !/^\d+$/.test(pre[0]) ? `${core.join('.')}-${pre[0]}` : null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* What may follow the highest line-shaped release of `versions`: {highest, family, wave, line}, or null when none has that shape.
|
|
104
|
-
* With a `candidate`, a family that already has released tags is judged alone (another family's tags, such as legacy 2.0.1-alpha.N,
|
|
105
|
-
* never outrank the candidate's own line); a family without a tag falls back to the highest release overall.
|
|
106
|
-
*/
|
|
107
|
-
export function nextReleases(versions, candidate = null) {
|
|
108
|
-
const shaped = versions.filter((version) => readable(version) && lineStart(waveOf(version).base));
|
|
109
|
-
const family = candidate === null ? null : familyOf(candidate);
|
|
110
|
-
const own = family === null ? [] : shaped.filter((version) => lineStart(waveOf(version).base).family === family);
|
|
111
|
-
const pool = own.length ? own : shaped;
|
|
112
|
-
if (!pool.length) return null;
|
|
113
|
-
const highest = pool.reduce((top, version) => (compareVersions(version, top) > 0 ? version : top));
|
|
114
|
-
const start = lineStart(waveOf(highest).base);
|
|
115
|
-
return { highest, family: start.family, wave: nextWave(highest, pool), line: `${start.family}.${start.line + 1}` };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Why `version` cannot be released after `released` (the versions already tagged), or null when it can: the next wave of the highest
|
|
120
|
-
* release of its own family, the first release of the next line, or (when its family has no tag) a later release of another label or
|
|
121
|
-
* core than the highest. The legacy `-fix` form is always refused.
|
|
122
|
-
*/
|
|
123
|
-
export function releaseVersionRefusal(version, released) {
|
|
124
|
-
const next = nextReleases(released.filter((entry) => entry !== version), version);
|
|
125
|
-
const named = next ? `; the next release is ${next.wave} (the next wave or fix after ${next.highest}) or ${next.line} (the next line)` : '';
|
|
126
|
-
if (waveOf(version).legacy) return `${version} is the legacy -fix form, which is read and never released again${named}`;
|
|
127
|
-
if (!next || version === next.wave || version === next.line) return null;
|
|
128
|
-
if (!readable(version)) return `${version} is not a SemVer version${named}`;
|
|
129
|
-
if (lineStart(waveOf(version).base)?.family !== next.family && compareVersions(version, next.highest) > 0) return null;
|
|
130
|
-
return `${version} does not follow ${next.highest}${named}`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** The versions of the release tags `v<version>` in the output of `git tag --list`. */
|
|
134
|
-
export const taggedVersions = (listing) => String(listing ?? '').split(/\r?\n/).map((name) => name.trim()).filter((name) => /^v\d/.test(name)).map((name) => name.slice(1));
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
/** The usable runtime linked for `home`, or null with a diagnostic in `skipped` for a stale or unreadable record. */
|
|
6
|
-
export function readRuntimeRecord({ home = os.homedir(), exists = existsSync, read = readFileSync, skipped = [] } = {}) {
|
|
7
|
-
const record = path.join(home, '.starci', 'runtime.json');
|
|
8
|
-
if (!exists(record)) return null;
|
|
9
|
-
let candidate;
|
|
10
|
-
try { candidate = JSON.parse(read(record, 'utf8'))?.root; }
|
|
11
|
-
catch {
|
|
12
|
-
skipped.push(`${record} is not valid JSON`);
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
if (typeof candidate === 'string' && candidate) {
|
|
16
|
-
const root = path.resolve(candidate);
|
|
17
|
-
if (exists(path.join(root, 'scripts', 'cli', 'main.mjs'))) return { root, source: record };
|
|
18
|
-
}
|
|
19
|
-
skipped.push(`${record} points at ${JSON.stringify(candidate ?? null)}, which has no scripts/cli/main.mjs (stale record)`);
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
// The root archive's names equal npm's selection from tracked inputs and verified generated copies.
|
|
2
|
-
import { byCodeUnit } from './list.mjs';
|
|
3
|
-
|
|
4
|
-
const relativeFile = (file) => typeof file === 'string' && file.length > 0 && !/^[a-z]:/i.test(file) && !file.includes('\0') && !file.includes('\\') && !file.split('/').some((part) => !part || part === '.' || part === '..');
|
|
5
|
-
|
|
6
|
-
/** Exact selected-set proof; source-byte equality remains owned by verifyPackedDependencies. */
|
|
7
|
-
export function runtimePackageInventory({ files, tracked, generated = [], selected }) {
|
|
8
|
-
if (!Array.isArray(tracked) || !tracked.length || !tracked.every(relativeFile)) return { unrun: 'the tracked file inventory of the root is unavailable or malformed' };
|
|
9
|
-
if (!Array.isArray(selected) || !selected.length || !selected.every(relativeFile) || new Set(selected).size !== selected.length) return { unrun: 'npm did not return a complete, unique tracked-projection inventory' };
|
|
10
|
-
if (!Array.isArray(generated) || !generated.every(relativeFile)) return { unrun: 'the verified generated file inventory is malformed' };
|
|
11
|
-
const trackedSet = new Set(tracked), generatedSet = new Set(generated), selectedSet = new Set(selected);
|
|
12
|
-
const unknown = selected.filter((file) => !trackedSet.has(file) && !generatedSet.has(file));
|
|
13
|
-
if (unknown.length) return { unrun: `npm selected files outside the tracked projection: ${unknown.slice(0, 5).join(', ')}` };
|
|
14
|
-
const packed = [...files.keys()];
|
|
15
|
-
const foreign = packed.filter((file) => !file.startsWith('package/') || !selectedSet.has(file.slice('package/'.length))).sort(byCodeUnit);
|
|
16
|
-
if (foreign.length) return { red: `root archive carries ${foreign.length} file(s) outside npm's tracked-only selection (ignored build output or scratch): ${foreign.slice(0, 5).join(', ')}` };
|
|
17
|
-
const missing = selected.filter((file) => !files.has(`package/${file}`)).sort(byCodeUnit);
|
|
18
|
-
if (missing.length) return { red: `root archive omits ${missing.length} file(s) from npm's tracked-only selection: ${missing.slice(0, 5).join(', ')}` };
|
|
19
|
-
const generatedCount = selected.filter((file) => !trackedSet.has(file) && generatedSet.has(file)).length;
|
|
20
|
-
return { tracked: selected.length - generatedCount, generated: generatedCount };
|
|
21
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// Manifests the root archive ships name files by path; a path that does not ship breaks the installed runtime.
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { parseYaml } from '../../engine/yaml.mjs';
|
|
5
|
-
import { asList, byCodeUnit } from './list.mjs';
|
|
6
|
-
import { EXAMPLES_ROOT } from './example-refs.mjs';
|
|
7
|
-
|
|
8
|
-
const KNOWLEDGE_SOURCE = 'starci/knowledge-source@1';
|
|
9
|
-
const strings = (value) => asList(value).filter((item) => typeof item === 'string' && item.length > 0);
|
|
10
|
-
const entries = (value) => asList(value).filter((item) => item && typeof item === 'object');
|
|
11
|
-
const joined = (directory, file) => path.posix.normalize(path.posix.join(directory, file));
|
|
12
|
-
|
|
13
|
-
/** The example catalog: each example's files/entrypoint/projects/tests are relative to that example's own directory. */
|
|
14
|
-
const exampleReferences = (doc) => entries(doc?.examples).flatMap((example) => {
|
|
15
|
-
const base = path.posix.join(EXAMPLES_ROOT, String(example.path ?? ''));
|
|
16
|
-
return [...strings(example.files), ...strings(example.entrypoint), ...strings(example.projects), ...strings(example.tests)]
|
|
17
|
-
.map((file) => ({ from: String(example.id ?? '?'), target: joined(base, file) }));
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
/** The schema catalog: schemas[].file and every moduleLocalDocumentKinds[].files entry are relative to the package root. */
|
|
21
|
-
const schemaReferences = (doc) => [
|
|
22
|
-
...entries(doc?.schemas).flatMap((schema) => strings(schema.file).map((file) => ({ from: String(schema.id ?? '?'), target: joined('.', file) }))),
|
|
23
|
-
...entries(doc?.moduleLocalDocumentKinds).flatMap((kind) => strings(kind.files).map((file) => ({ from: String(kind.id ?? '?'), target: joined('.', file) }))),
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
/** The path-bearing keys of a grammar family entry. */
|
|
27
|
-
const FAMILY_PATH_KEYS = ['authority', 'packageSnapshot', 'reusableStyle', 'productComposition', 'index'];
|
|
28
|
-
|
|
29
|
-
/** A knowledge-source index: branches[].path, topics[].path and the family files, relative to the index's own directory. */
|
|
30
|
-
const knowledgeReferences = (directory) => (doc) => [
|
|
31
|
-
...entries(doc?.branches).flatMap((branch) => strings(branch.path).map((file) => ({ from: String(branch.id ?? '?'), target: joined(directory, file) }))),
|
|
32
|
-
...entries(doc?.topics).flatMap((topic) => strings(topic.path).map((file) => ({ from: String(topic.title ?? '?'), target: joined(directory, file) }))),
|
|
33
|
-
...entries(doc?.families).flatMap((family) => FAMILY_PATH_KEYS.flatMap((key) => strings(family[key]).map((file) => ({ from: `${family.id ?? '?'}.${key}`, target: joined(directory, file) })))),
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
/** Manifests the package must ship; their absence from the selection is red, never a skip. */
|
|
37
|
-
export const REQUIRED_MANIFESTS = Object.freeze([`${EXAMPLES_ROOT}/index.yaml`, 'modules/schemas/index.yaml', 'knowledge/index.yaml']);
|
|
38
|
-
|
|
39
|
-
const KNOWLEDGE_INDEX = /^knowledge\/(?:.+\/)?index\.yaml$/;
|
|
40
|
-
|
|
41
|
-
/** The reader of one manifest file: the two catalogs have their own, every knowledge index reads its own directory. */
|
|
42
|
-
const readerOf = (file) => {
|
|
43
|
-
if (file === REQUIRED_MANIFESTS[0]) return exampleReferences;
|
|
44
|
-
if (file === REQUIRED_MANIFESTS[1]) return schemaReferences;
|
|
45
|
-
return knowledgeReferences(path.posix.dirname(file));
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/** The manifests to read: the required set plus every selected knowledge index. */
|
|
49
|
-
const manifestFiles = (selected) => [...new Set([...REQUIRED_MANIFESTS, ...selected.filter((name) => KNOWLEDGE_INDEX.test(name))])].sort(byCodeUnit);
|
|
50
|
-
|
|
51
|
-
/** {references} of one manifest, {skip} for an index of another schema, {unrun} or {red} when it cannot be trusted. */
|
|
52
|
-
function readManifest(file, readFile) {
|
|
53
|
-
let doc;
|
|
54
|
-
try { doc = parseYaml(readFile(file)); } catch { return { unrun: `${file} is unreadable or not valid YAML` }; }
|
|
55
|
-
if (doc?.schema !== KNOWLEDGE_SOURCE && !REQUIRED_MANIFESTS.includes(file)) return { skip: true };
|
|
56
|
-
const references = readerOf(file)(doc);
|
|
57
|
-
if (!references.length) return { red: `${file} yields no file references; its reference keys changed or it lists nothing, so nothing in it would be checked` };
|
|
58
|
-
return { references };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Pre-pack admission: every path a shipped manifest names must be in the archive selection. Manifests are the required
|
|
63
|
-
* set plus every selected knowledge index stamped starci/knowledge-source@1, so a new index is covered unlisted.
|
|
64
|
-
* Returns {unrun} when a manifest cannot be read, {red} naming the first unresolved paths, else {manifests, references, counts}.
|
|
65
|
-
*/
|
|
66
|
-
export function manifestAdmission({ root, selected, readFile = (file) => fs.readFileSync(path.join(root, file), 'utf8') }) {
|
|
67
|
-
const shipped = new Set(selected);
|
|
68
|
-
const missing = REQUIRED_MANIFESTS.filter((file) => !shipped.has(file));
|
|
69
|
-
if (missing.length) return { red: `the archive does not ship required manifest(s): ${missing.join(', ')}` };
|
|
70
|
-
const gaps = [];
|
|
71
|
-
const counts = {};
|
|
72
|
-
for (const file of manifestFiles(selected)) {
|
|
73
|
-
const read = readManifest(file, readFile);
|
|
74
|
-
if (read.unrun || read.red) return read;
|
|
75
|
-
if (read.skip) continue;
|
|
76
|
-
counts[file] = read.references.length;
|
|
77
|
-
for (const { from, target } of read.references) if (target.startsWith('../') || !shipped.has(target)) gaps.push(`${file} (${from}) -> ${target}`);
|
|
78
|
-
}
|
|
79
|
-
if (gaps.length) {
|
|
80
|
-
const sorted = [...new Set(gaps)].sort(byCodeUnit);
|
|
81
|
-
return { red: `${sorted.length} shipped manifest reference(s) do not resolve in the archive: ${sorted.slice(0, 5).join('; ')}` };
|
|
82
|
-
}
|
|
83
|
-
return { manifests: Object.keys(counts).length, references: Object.values(counts).reduce((sum, count) => sum + count, 0), counts };
|
|
84
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
// The release owner's dist-tag judgment and idempotent reconciliation, independent of npm processes.
|
|
2
|
-
import { compareVersions, releaseLine, versionParts } from './release-version.mjs';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Plan the release's declared tags from a complete registry read. Unknown reads and historical
|
|
6
|
-
* releases fail closed (alpha.31.1 is behind alpha.31.2 and alpha.32 on the one alpha line); a newer
|
|
7
|
-
* tag on another release line is preserved rather than rolled back.
|
|
8
|
-
* Returns serializable {tag, version, current, action} entries for the native dry-run plan.
|
|
9
|
-
*/
|
|
10
|
-
export function runtimeTagPlan({ version, tags, state, uploadTag = null }) {
|
|
11
|
-
versionParts(version);
|
|
12
|
-
if (!Array.isArray(tags) || !tags.length || tags.some((tag) => typeof tag !== 'string' || !/^[a-z][a-z0-9-]*$/i.test(tag)))
|
|
13
|
-
throw new Error('runtime publication requires declared npm dist-tags');
|
|
14
|
-
if (!['present', 'absent'].includes(state?.state) || !Array.isArray(state.versions) || !state.distTags || Array.isArray(state.distTags) || typeof state.distTags !== 'object')
|
|
15
|
-
throw new Error('runtime dist-tags and version inventory could not be read');
|
|
16
|
-
if (state.state === 'present' && !state.versions.includes(version)) throw new Error('runtime version inventory omits the published release');
|
|
17
|
-
const newer = state.versions.find((entry) => releaseLine(entry) === releaseLine(version) && compareVersions(entry, version) > 0);
|
|
18
|
-
if (newer) throw new Error(`runtime release ${version} is behind ${newer} on its release line; dist-tags cannot move backwards`);
|
|
19
|
-
const plan = [...new Set(tags)].map((tag) => {
|
|
20
|
-
const current = state.distTags[tag] ?? null;
|
|
21
|
-
if (current !== null && typeof current !== 'string') throw new Error(`unreadable npm dist-tag ${tag}`);
|
|
22
|
-
const compared = current === null ? -1 : compareVersions(current, version);
|
|
23
|
-
let action = current === version ? 'confirmed' : 'set';
|
|
24
|
-
if (compared > 0) action = 'preserve';
|
|
25
|
-
return { tag, version: compared > 0 ? current : version, current,
|
|
26
|
-
action };
|
|
27
|
-
});
|
|
28
|
-
if (uploadTag && plan.some((entry) => entry.tag === uploadTag && entry.action === 'preserve'))
|
|
29
|
-
throw new Error(`runtime upload channel ${uploadTag} points to a newer release; publication cannot move it backwards`);
|
|
30
|
-
return plan;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Repair only stale declared tags of an integrity-confirmed runtime version, then read every tag
|
|
35
|
-
* back. The registry seam owns effects; a failed write/read throws and can be retried without upload.
|
|
36
|
-
*/
|
|
37
|
-
export function reconcileRuntimeTags({ row, tags, registry, out, sleep = null, waitMs = 0, stepMs = 5000 }) {
|
|
38
|
-
const state = registry.state(row.name, row.version);
|
|
39
|
-
if (state.state !== 'present' || state.integrity !== row.registry.integrity) throw new Error('runtime registry integrity changed before dist-tag reconciliation');
|
|
40
|
-
const plan = runtimeTagPlan({ version: row.version, tags, state });
|
|
41
|
-
for (const entry of plan.filter((item) => item.action === 'set')) {
|
|
42
|
-
const result = registry.tag(row.name, row.version, entry.tag);
|
|
43
|
-
if (!result.ok) throw new Error(`npm dist-tag ${entry.tag} failed (exit ${result.status ?? 'unknown'})`);
|
|
44
|
-
}
|
|
45
|
-
// Registry propagation of a tag write can lag; poll the read-back inside the window before calling it stale.
|
|
46
|
-
const deadline = Date.now() + waitMs;
|
|
47
|
-
let confirmed = registry.state(row.name, row.version);
|
|
48
|
-
while (sleep && Date.now() < deadline && plan.some((entry) => confirmed.distTags?.[entry.tag] !== entry.version)) {
|
|
49
|
-
sleep(stepMs);
|
|
50
|
-
confirmed = registry.state(row.name, row.version);
|
|
51
|
-
}
|
|
52
|
-
if (confirmed.state !== 'present' || confirmed.integrity !== state.integrity || plan.some((entry) => confirmed.distTags?.[entry.tag] !== entry.version))
|
|
53
|
-
throw new Error('runtime dist-tag read-back did not confirm the declared targets and immutable integrity');
|
|
54
|
-
for (const entry of plan) out(` dist-tag ${row.name}@${entry.version} ${entry.tag}: ${entry.action === 'preserve' ? 'newer release preserved' : 'confirmed by registry read-back'}`);
|
|
55
|
-
return plan;
|
|
56
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// seat-env.mjs — the seat identity one Orca terminal's environment claims, and the env a runtime child gets.
|
|
2
|
-
//
|
|
3
|
-
// Orca exports the terminal's identity into the shell it owns (ORCA_TERMINAL_HANDLE, ORCA_PANE_KEY, ORCA_TAB_ID,
|
|
4
|
-
// ORCA_WORKTREE_ID, ORCA_AGENT_* — the agent hook endpoint and token included), and a session can carry the caller
|
|
5
|
-
// markers STARCI_ROLE, STARCI_GUARD_FILE and STARCI_CALLER. Caller and guard logic binds a process to a seat by those
|
|
6
|
-
// variables (scripts/guards/op-caller.mjs callerOf, scripts/guards/command-policy.mjs shimDecision, the seat bindings of
|
|
7
|
-
// scripts/guards/hook-install.mjs), so a child the runtime spawns for its own work — the package proof of the land gate,
|
|
8
|
-
// the scaffold e2e, a staging `npm ci` and its lifecycle scripts — that inherits them is admitted as
|
|
9
|
-
// the seat: a supervisor seat's `node install.js` reads as a raw supervisor tool call (RIGHTS_RAW_TOOL) and a spec's
|
|
10
|
-
// kernel call resolves the seat's custody (kernel-caller-unknown). The seat's own agent tool calls keep the identity;
|
|
11
|
-
// every other child of it gets this env.
|
|
12
|
-
//
|
|
13
|
-
// The seat's PATH can likewise carry the per-user guard-shim directory first (<home>/.starci/bin,
|
|
14
|
-
// packages/cli/src/shim.mjs writeRuntimeShim — put there by scripts/agent/starci-shim.mjs addStarciShimToPath): its
|
|
15
|
-
// wrappers judge `node`, `git`, `npm` and friends through shimDecision before reaching the real tool. Without the
|
|
16
|
-
// seat identity a wrapper binds no role and passes through, but a runtime child that never answers to the seat —
|
|
17
|
-
// the codex app-server probe, whose `codex` launcher resolves `node` through PATH — also gets PATH without the shim
|
|
18
|
-
// directory, so nothing it spawns is ever re-judged.
|
|
19
|
-
import os from 'node:os';
|
|
20
|
-
import path from 'node:path';
|
|
21
|
-
|
|
22
|
-
/** The variables that bind a process to the calling seat or claim a caller identity. */
|
|
23
|
-
const SEAT_ENV_VARS = Object.freeze(['ORCA_TERMINAL_HANDLE', 'ORCA_PANE_KEY', 'ORCA_TAB_ID', 'ORCA_WORKTREE_ID', 'STARCI_ROLE', 'STARCI_GUARD_FILE', 'STARCI_CALLER']);
|
|
24
|
-
|
|
25
|
-
/** `parent` (the caller's env) without the seat identity: a child that is not the seat's own agent tool call runs unbound. Pure — the caller names the env; the base tier never reads process.env itself (RT_BASE_IMPURE). */
|
|
26
|
-
export function withoutSeatEnv(parent) {
|
|
27
|
-
const env = { ...parent };
|
|
28
|
-
for (const key of Object.keys(env)) if (SEAT_ENV_VARS.includes(key) || key.startsWith('ORCA_AGENT_')) delete env[key];
|
|
29
|
-
return env;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** `parent` without the guard-shim directory on PATH (any casing of the PATH key, the one <home>/.starci/bin entry wherever it sits). Pure — the caller names the env; `home`/`platform` are injectable seams. */
|
|
33
|
-
export function withoutSeatShim(parent, { home = os.homedir(), platform = process.platform } = {}) {
|
|
34
|
-
const env = { ...parent };
|
|
35
|
-
const key = Object.keys(env).find((name) => name.toLowerCase() === 'path');
|
|
36
|
-
if (!key) return env;
|
|
37
|
-
const shim = path.resolve(path.join(home, '.starci', 'bin'));
|
|
38
|
-
const resolved = (entry) => path.resolve(String(entry).replace(/^"(.*)"$/, '$1'));
|
|
39
|
-
const same = (entry) => platform === 'win32' ? resolved(entry).toLowerCase() === shim.toLowerCase() : resolved(entry) === shim;
|
|
40
|
-
const delimiter = platform === 'win32' ? ';' : ':';
|
|
41
|
-
env[key] = String(env[key]).split(delimiter).filter((entry) => entry && !same(entry)).join(delimiter);
|
|
42
|
-
return env;
|
|
43
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
// sonar-new-code.mjs - the New Code definition a Sonar project is ensured with, and how each provider stores and reads it back. Pure.
|
|
2
|
-
// Without a New Code definition a SonarQube Cloud project's gate status is NONE and the scanner's quality-gate wait reports the gate
|
|
3
|
-
// "not set" (alpha.27: two example projects carried the gate but no New Code definition). The canonical
|
|
4
|
-
// period is knowledge/sonar-gate.yaml `gate.newCodePeriod` of the repository's edition; a stack target may declare its own
|
|
5
|
-
// `resource.newCodePeriod` of the same shape. SonarQube Server and Community Build store it through api/new_code_periods; SonarQube
|
|
6
|
-
// Cloud has no such web service and keeps it as the project setting `sonar.leak.period` (a number of days, or `previous_version`).
|
|
7
|
-
|
|
8
|
-
/** The period types a declaration may name, and whether each carries a value. */
|
|
9
|
-
const NEW_CODE_TYPES = Object.freeze({ NUMBER_OF_DAYS: 'days', PREVIOUS_VERSION: 'none' });
|
|
10
|
-
/** The setting SonarQube Cloud keeps a project's New Code definition in. */
|
|
11
|
-
export const CLOUD_NEW_CODE_SETTING = 'sonar.leak.period';
|
|
12
|
-
const MAX_DAYS = 90;
|
|
13
|
-
|
|
14
|
-
/** A period of a closed shape: {type: NUMBER_OF_DAYS, value: 1..90} or {type: PREVIOUS_VERSION}; throws on anything else. */
|
|
15
|
-
export function newCodePeriodOf(period) {
|
|
16
|
-
if (!period || typeof period !== 'object' || Array.isArray(period)) throw new Error('Invalid declared new code period');
|
|
17
|
-
const keys = Object.keys(period);
|
|
18
|
-
if (!keys.every((key) => key === 'type' || key === 'value') || !Object.hasOwn(NEW_CODE_TYPES, period.type)) throw new Error('Invalid declared new code period');
|
|
19
|
-
if (period.type === 'PREVIOUS_VERSION') {
|
|
20
|
-
if (period.value !== undefined) throw new Error('Invalid declared new code period');
|
|
21
|
-
return { type: 'PREVIOUS_VERSION' };
|
|
22
|
-
}
|
|
23
|
-
if (!Number.isInteger(period.value) || period.value < 1 || period.value > MAX_DAYS) throw new Error('Invalid declared new code period');
|
|
24
|
-
return { type: 'NUMBER_OF_DAYS', value: period.value };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** The period a project target is ensured with: its own `resource.newCodePeriod` when declared, else the canonical one of the gate. */
|
|
28
|
-
export function declaredNewCodePeriod(resource, canonical) {
|
|
29
|
-
return newCodePeriodOf(resource?.newCodePeriod ?? canonical);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** The value SonarQube Cloud stores in `sonar.leak.period` for `period`. */
|
|
33
|
-
export const cloudLeakValue = (period) => (period.type === 'PREVIOUS_VERSION' ? 'previous_version' : String(period.value));
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* The write that sets `period` on `projectKey` for `provider`: {kind: 'setting', key, value} on SonarQube Cloud, else
|
|
37
|
-
* {kind: 'period', form: {project, type, value?}} for api/new_code_periods/set.
|
|
38
|
-
*/
|
|
39
|
-
export function newCodeWrite(provider, projectKey, period) {
|
|
40
|
-
if (provider === 'sonar-cloud') return { kind: 'setting', key: CLOUD_NEW_CODE_SETTING, value: cloudLeakValue(period) };
|
|
41
|
-
return { kind: 'period', form: { project: projectKey, type: period.type, ...(period.value === undefined ? {} : { value: String(period.value) }) } };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Whether a provider's readback holds `period`: on SonarQube Cloud the api/settings/values answer ({settings: [{key, value}]}),
|
|
46
|
-
* elsewhere the api/new_code_periods/show answer ({type, value}). A missing definition, another type or another value is no match.
|
|
47
|
-
*/
|
|
48
|
-
export function newCodeMatches(provider, readback, period) {
|
|
49
|
-
if (provider === 'sonar-cloud') {
|
|
50
|
-
const row = (readback?.settings ?? []).find((setting) => setting?.key === CLOUD_NEW_CODE_SETTING);
|
|
51
|
-
return row?.value === cloudLeakValue(period);
|
|
52
|
-
}
|
|
53
|
-
if (readback?.type !== period.type) return false;
|
|
54
|
-
return period.value === undefined || String(readback.value) === String(period.value);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** The public proof of an ensured period: `NUMBER_OF_DAYS:30` or `PREVIOUS_VERSION`. */
|
|
58
|
-
export const newCodeProof = (period) => (period.value === undefined ? period.type : `${period.type}:${period.value}`);
|