@starci/hfs 4.0.10 → 4.2.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 +5 -1
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +65 -24
- package/package.json +1 -1
- package/report/order.mjs +2 -0
- package/runtime/config.example.yaml +194 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +222 -239
- package/runtime/engine/invalid-config.mjs +5 -5
- package/runtime/engine/model-config.mjs +90 -0
- package/runtime/engine/orca-config.mjs +3 -1
- package/runtime/engine/release-config.mjs +28 -0
- package/runtime/engine/removed-vocabulary.mjs +28 -0
- package/runtime/engine/resources-config.mjs +52 -0
- package/runtime/engine/runtime-root.mjs +17 -0
- package/runtime/engine/secrets.mjs +35 -31
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
- package/runtime/knowledge/hfs/rules.yaml +161 -65
- package/runtime/knowledge/hfs/slots.yaml +11 -14
- package/runtime/knowledge/patterns/be/api.yaml +7 -24
- package/runtime/knowledge/patterns/be/cli.yaml +3 -12
- package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
- package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
- package/runtime/modules/kernel/failure-codes.yaml +1 -41
- package/runtime/modules/kernel/removed-vocabulary.yaml +268 -0
- package/runtime/modules/models/registry.yaml +25 -359
- package/runtime/modules/models/runtimes.yaml +126 -265
- package/runtime/modules/models/tiers.yaml +66 -0
- package/runtime/scripts/api/fs/claim-file.mjs +30 -0
- package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
- package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
- package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
- package/runtime/scripts/api/git/lib.mjs +34 -1
- package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
- package/runtime/scripts/api/process/run-program.mjs +2 -1
- package/runtime/scripts/api/sops/lib.mjs +198 -120
- package/runtime/scripts/hfs/allows.mjs +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
- package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
- package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
- package/runtime/scripts/hfs/architecture/config.mjs +95 -80
- package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
- package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
- package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
- package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
- package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
- package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
- package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
- package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
- package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
- package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
- package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
- package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
- package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
- package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
- package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
- package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
- package/runtime/scripts/hfs/architecture/index.mjs +164 -104
- package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
- package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
- package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
- package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
- package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
- package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
- package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
- package/runtime/scripts/hfs/architecture/required-files.mjs +121 -70
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
- package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
- package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
- package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
- package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
- package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
- package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
- package/runtime/scripts/hfs/architecture.mjs +3 -4
- package/runtime/scripts/hfs/catalog-once.mjs +15 -0
- package/runtime/scripts/hfs/check.mjs +63 -71
- package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
- package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
- package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
- package/runtime/scripts/hfs/edition-slots.mjs +5 -3
- package/runtime/scripts/hfs/linear-text.mjs +30 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
- package/runtime/scripts/hfs/path-findings.mjs +58 -38
- package/runtime/scripts/hfs/pin-findings.mjs +31 -0
- package/runtime/scripts/hfs/repo-identity.mjs +21 -4
- package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
- package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
- package/runtime/scripts/hfs/rules/cli.mjs +5 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
- package/runtime/scripts/hfs/rules/contract.mjs +35 -27
- package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
- package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
- package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
- package/runtime/scripts/hfs/rules/database.mjs +36 -23
- package/runtime/scripts/hfs/rules/deps.mjs +64 -32
- package/runtime/scripts/hfs/rules/docker.mjs +113 -65
- package/runtime/scripts/hfs/rules/edition.mjs +118 -111
- package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
- package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
- package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
- package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
- package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
- package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
- package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
- package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
- package/runtime/scripts/hfs/rules/saga.mjs +128 -61
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
- package/runtime/scripts/hfs/rules/services.mjs +34 -16
- package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
- package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
- package/runtime/scripts/hfs/secret.mjs +59 -48
- package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
- package/runtime/scripts/hfs/slot-classify.mjs +65 -0
- package/runtime/scripts/hfs/slot-errors.mjs +11 -0
- package/runtime/scripts/hfs/slot-imports.mjs +76 -0
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
- package/runtime/scripts/hfs/slot-match.mjs +153 -0
- package/runtime/scripts/hfs/slot-path.mjs +8 -0
- package/runtime/scripts/hfs/slot-required.mjs +76 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
- package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
- package/runtime/scripts/hfs/slots.mjs +74 -695
- package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
- package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
- package/runtime/scripts/hfs/tree.mjs +19 -14
- package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
- package/runtime/scripts/hfs/view.mjs +1 -1
- package/runtime/scripts/lib/dockerfile.mjs +56 -33
- package/runtime/scripts/lib/env.mjs +10 -0
- package/runtime/scripts/lib/event-contract.mjs +17 -13
- package/runtime/scripts/lib/git.mjs +2 -2
- package/runtime/scripts/lib/graphql-contract.mjs +158 -322
- package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
- package/runtime/scripts/lib/i18n.mjs +3 -2
- package/runtime/scripts/lib/in-order.mjs +71 -0
- package/runtime/scripts/lib/language.mjs +3 -3
- package/runtime/scripts/lib/list.mjs +8 -1
- package/runtime/scripts/lib/path-key.mjs +38 -5
- package/runtime/scripts/lib/pid-alive.mjs +7 -0
- package/runtime/scripts/lib/regex.mjs +1 -1
- package/runtime/scripts/lib/same-text.mjs +1 -1
- package/runtime/scripts/lib/secret-patterns.mjs +6 -2
- package/runtime/scripts/lib/sleep-sync.mjs +2 -2
- package/runtime/scripts/lib/sops-envelope.mjs +95 -48
- package/runtime/scripts/lib/stack-services.mjs +1 -2
- package/runtime/scripts/lib/ts-ast.mjs +9 -0
- package/runtime/scripts/lib/walk.mjs +6 -1
- package/runtime/scripts/lib/yaml-cached.mjs +14 -0
- package/scaffold/add-cli-lite.mjs +2 -1
- package/scaffold/add-table.mjs +2 -2
- package/scaffold/add.mjs +3 -2
- package/scaffold/app.mjs +4 -2
- package/scaffold/edition-gate.mjs +25 -25
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +5 -4
- package/sync/hygiene.mjs +15 -9
- package/sync/index.mjs +7 -3
- package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
- package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
- package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
- package/templates/app/ci-workflows/sonar-steps.yml +2 -2
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
- package/templates/app/starciwork.gitignore +1 -1
- package/templates/be/image/api/Dockerfile +1 -1
- package/templates/be/image/cli/Dockerfile +1 -1
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
- package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/fe/image/next/Dockerfile +2 -1
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
- package/upgrade/index.mjs +9 -2
- package/runtime/engine/admission.mjs +0 -308
- package/runtime/engine/canonical-json.mjs +0 -10
- package/runtime/engine/db/blob.mjs +0 -315
- package/runtime/engine/db/ledger-paths.mjs +0 -83
- package/runtime/engine/db/ledger.mjs +0 -1118
- package/runtime/engine/db/machine-connection.mjs +0 -135
- package/runtime/engine/db/machine-schema.mjs +0 -84
- package/runtime/engine/db/machine.mjs +0 -1367
- package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
- package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
- package/runtime/engine/db/provider-reservations.mjs +0 -101
- package/runtime/engine/digest.mjs +0 -16
- package/runtime/engine/refuse.mjs +0 -11
- package/runtime/modules/ops/_labels.yaml +0 -53
- package/runtime/scripts/api/node/lib.mjs +0 -14
- package/runtime/scripts/api/node/spawn-node.mjs +0 -6
- package/runtime/scripts/api/process/lib.mjs +0 -111
- package/runtime/scripts/api/process/owned-process.mjs +0 -78
- package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
- package/runtime/scripts/connectors/lib.mjs +0 -488
- package/runtime/scripts/lib/clip.mjs +0 -19
- package/runtime/scripts/lib/display-names.mjs +0 -259
- package/runtime/scripts/lib/example-refs.mjs +0 -158
- package/runtime/scripts/lib/json-schema.mjs +0 -52
- package/runtime/scripts/lib/process-identity.mjs +0 -6
- package/runtime/scripts/lib/read-yaml.mjs +0 -18
- package/runtime/scripts/lib/redact.mjs +0 -161
- package/runtime/scripts/lib/sleep.mjs +0 -7
- package/runtime/scripts/lib/source-phrases.mjs +0 -34
- package/runtime/scripts/lib/sqlite.mjs +0 -21
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import "server-only"
|
|
2
2
|
import { hasLocale } from "next-intl"
|
|
3
3
|
import { getRequestConfig } from "next-intl/server"
|
|
4
|
+
import { headers } from "next/headers"
|
|
4
5
|
import { PRODUCT_TIME_ZONE, routing } from "./routing"
|
|
5
6
|
|
|
6
|
-
/** The
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/** The header the next-intl middleware sets on the request with the locale it negotiated. */
|
|
8
|
+
const LOCALE_HEADER = "x-next-intl-locale"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The request config next-intl's plugin loads. The locale is the one the middleware negotiated: this module is reachable from the proxy
|
|
12
|
+
* through the navigation, and `next/root-params` can only be imported inside the app directory.
|
|
13
|
+
*/
|
|
14
|
+
export default getRequestConfig(async () => {
|
|
15
|
+
const requested = (await headers()).get(LOCALE_HEADER)
|
|
9
16
|
const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale
|
|
10
17
|
return {
|
|
11
18
|
locale,
|
package/upgrade/index.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "../runtime/scripts/hfs/slots.mjs";
|
|
14
14
|
import { managedScriptNames } from "../runtime/scripts/hfs/architecture/managed-scripts.mjs";
|
|
15
15
|
import { LOCK_STEP, npmLock, scaffoldWorkSeeds } from "../scaffold/app.mjs";
|
|
16
|
+
import { byCodeUnit } from "../report/order.mjs";
|
|
16
17
|
import {
|
|
17
18
|
TEMPLATES_DIR,
|
|
18
19
|
appSource,
|
|
@@ -263,11 +264,13 @@ function sourceWithAdditions(root, additions) {
|
|
|
263
264
|
.map((file) => [file.path.slice("be/".length), file.content]),
|
|
264
265
|
);
|
|
265
266
|
return {
|
|
266
|
-
files: [...new Set([...current.files, ...added.keys()])].sort(),
|
|
267
|
+
files: [...new Set([...current.files, ...added.keys()])].sort(byCodeUnit),
|
|
267
268
|
read: (file) => added.get(file) ?? current.read(file),
|
|
268
269
|
};
|
|
269
270
|
}
|
|
270
271
|
|
|
272
|
+
// Files the app authors itself: the upgrade never overwrites them and keeps an existing one (CONTEXT: declarations are preserved).
|
|
273
|
+
const APP_OWNED = new Set([".starcistacks/application-stacks.yaml"]);
|
|
271
274
|
const publicStep = ({ op, path: file, why }) => ({ op, path: file, why });
|
|
272
275
|
|
|
273
276
|
async function model({ root, to, presets, manifest }) {
|
|
@@ -353,6 +356,10 @@ async function model({ root, to, presets, manifest }) {
|
|
|
353
356
|
for (const file of desired) {
|
|
354
357
|
const target = path.join(root, ...file.path.split("/"));
|
|
355
358
|
if (fs.existsSync(target)) {
|
|
359
|
+
if (APP_OWNED.has(file.path)) {
|
|
360
|
+
steps.push({ op: "keep", path: file.path, why: "The app already declares its stacks; the declaration stays as the app wrote it.", kind: "keep" });
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
356
363
|
if (!sameText(root, file.path, file.content))
|
|
357
364
|
throw new UpgradeError(
|
|
358
365
|
"HFS_UPGRADE_FILE_CONFLICT",
|
|
@@ -462,7 +469,7 @@ export async function upgradeEdition({
|
|
|
462
469
|
const directories = [];
|
|
463
470
|
try {
|
|
464
471
|
for (const step of built.steps) {
|
|
465
|
-
if (step.kind === "lock") continue;
|
|
472
|
+
if (step.kind === "lock" || step.kind === "keep") continue;
|
|
466
473
|
if (step.kind === "managed") {
|
|
467
474
|
const target = built.managed.find(
|
|
468
475
|
(candidate) => candidate.path === step.path,
|
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { refuse } from './refuse.mjs';
|
|
3
|
-
|
|
4
|
-
const PATH_LEASE_PREFIX='path:';
|
|
5
|
-
const GLOB_META=/[*?[\]{}]/;
|
|
6
|
-
// Next.js App Router spells route segments as literal directory names: dynamic `[lang]`, catch-all
|
|
7
|
-
// `[...slug]` and optional catch-all `[[...opt]]`, optionally behind an intercept prefix `(.)`, `(..)`,
|
|
8
|
-
// `(...)` or `(..)(..)`. Route groups `(group)`, parallel slots `@slot` and intercepts on a static name
|
|
9
|
-
// carry no glob meta at all. A segment of exactly this shape is a concrete name, never a character class;
|
|
10
|
-
// every consumer that hands an owned path to a glob engine escapes it (git: ownedPathspec below).
|
|
11
|
-
const APP_ROUTER_SEGMENT=/^(?:\(\.{1,3}\))*(?:\[\[\.\.\.[A-Za-z0-9_$-]+\]\]|\[(?:\.\.\.)?[A-Za-z0-9_$-]+\])$/;
|
|
12
|
-
|
|
13
|
-
/** A Next.js App Router bracket segment (`[id]`, `[...slug]`, `[[...opt]]`, `(.)[id]`) — a literal directory name. */
|
|
14
|
-
export const isAppRouterSegment=part=>APP_ROUTER_SEGMENT.test(String(part??''));
|
|
15
|
-
|
|
16
|
-
/** A path segment that is a real glob (`*`, `?`, `{a,b}`, a bare character class), not an App Router name. */
|
|
17
|
-
export const isGlobSegment=part=>GLOB_META.test(String(part??''))&&!isAppRouterSegment(part);
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* The git pathspec for one concrete owned path. Git reads a plain pathspec as a glob, so `src/app/[id]`
|
|
21
|
-
* would also match a sibling `src/app/i`; `:(literal)` pins it to the named directory. Admission
|
|
22
|
-
* refuses a glob, so every owned path is literal.
|
|
23
|
-
*/
|
|
24
|
-
export const ownedPathspec=spec=>`:(literal)${String(spec??'').replace(/\\/g,'/')||'.'}`;
|
|
25
|
-
|
|
26
|
-
const plainPath=value=>typeof value==='string'?value:value?.path;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Canonical workspace-relative path prefix used by planning, leases and report boundaries. In a
|
|
30
|
-
* multi-repository project the caller includes the repository binding prefix (for example `my-app/`).
|
|
31
|
-
* A directory prefix is spelled either bare (`docs/`) or with a trailing `/**`, which normalizes to
|
|
32
|
-
* the same prefix; every other glob, absolute path and parent traversal is refused because it is not
|
|
33
|
-
* a concrete ownership boundary. A Next.js App Router segment (`[lang]`, `[...slug]`, `[[...opt]]`,
|
|
34
|
-
* `(group)`, `@slot`, `(.)photo`) is a literal directory name and is admitted as one.
|
|
35
|
-
*/
|
|
36
|
-
export function normalizeOwnedPath(value){
|
|
37
|
-
let input=String(plainPath(value)??'').trim().replace(/\\/g,'/');
|
|
38
|
-
input=input.replace(/\/\*\*\/$/,'').replace(/\/\*\*$/,'');
|
|
39
|
-
if(!input||input.startsWith('/')||/^[A-Za-z]:\//.test(input))throw Error(`owned path must be repository-relative: ${JSON.stringify(plainPath(value)??value)}`);
|
|
40
|
-
const parts=[];
|
|
41
|
-
for(const part of input.split('/')){
|
|
42
|
-
if(!part||part==='.')continue;
|
|
43
|
-
if(part==='..')throw Error(`owned path must not traverse its repository: ${JSON.stringify(plainPath(value)??value)}`);
|
|
44
|
-
if(isGlobSegment(part))throw Error(`owned path must be a concrete prefix, not a glob: ${JSON.stringify(plainPath(value)??value)}`);
|
|
45
|
-
parts.push(part);
|
|
46
|
-
}
|
|
47
|
-
if(!parts.length)throw Error(`owned path must name a concrete repository-relative prefix: ${JSON.stringify(plainPath(value)??value)}`);
|
|
48
|
-
return parts.join('/');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Normalize, de-duplicate and collapse descendants already covered by an owned ancestor. */
|
|
52
|
-
export function normalizeOwnedPaths(values=[]){
|
|
53
|
-
const paths=[...new Set(values.map(normalizeOwnedPath))].sort((a,b)=>a.length-b.length||a.localeCompare(b));
|
|
54
|
-
return paths.filter((candidate,index)=>!paths.slice(0,index).some(parent=>ownedPathsIntersect(parent,candidate)));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Path-prefix overlap: equality or either concrete path being below the other. */
|
|
58
|
-
export function ownedPathsIntersect(left,right){
|
|
59
|
-
const a=normalizeOwnedPath(left),b=normalizeOwnedPath(right);
|
|
60
|
-
return a===b||a.startsWith(`${b}/`)||b.startsWith(`${a}/`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** The durable resource identity for one normalized concrete owned path. */
|
|
64
|
-
export const ownedPathLeaseKey=value=>`${PATH_LEASE_PREFIX}${normalizeOwnedPath(value)}`;
|
|
65
|
-
|
|
66
|
-
/** One capacity-one request per minimal owned prefix. */
|
|
67
|
-
export const ownedPathLeaseRequests=values=>normalizeOwnedPaths(values).map(path=>({resourceKey:ownedPathLeaseKey(path),units:1}));
|
|
68
|
-
|
|
69
|
-
const leasePath=resourceKey=>String(resourceKey??'').startsWith(PATH_LEASE_PREFIX)
|
|
70
|
-
?String(resourceKey).slice(PATH_LEASE_PREFIX.length):null;
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* The spelling two lease paths are compared in. The same file must compare equal however a workflow
|
|
74
|
-
* spelled it (a past incident had `apps/app/src/messages/vi.json` spelled bare for
|
|
75
|
-
* the fe repository while the repository-prefixed spelling of the same file never overlapped it). `canonicalOf`
|
|
76
|
-
* (scripts/kernel/lease-canon.mjs) resolves a path to its app-relative form in a bound app
|
|
77
|
-
* (be/<path>, fe/<path>) — for a held row through its holder job, so every spelling of one file
|
|
78
|
-
* compares as one key; paths on Windows compare case-insensitively, as its file
|
|
79
|
-
* systems do.
|
|
80
|
-
*/
|
|
81
|
-
export const leaseCompareForm=(leasePathValue,{canonicalOf=null,row=null,platform=process.platform}={})=>{
|
|
82
|
-
let value=normalizeOwnedPath(leasePathValue);
|
|
83
|
-
if(canonicalOf){try{value=normalizeOwnedPath(canonicalOf(value,row)??value);}catch{/* an unresolvable spelling compares as written */}}
|
|
84
|
-
return platform==='win32'?value.toLowerCase():value;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Find durable path leases that overlap a requested parent/child prefix. Lease-row existence is the
|
|
89
|
-
* fence; expiry is only a recovery signal and does not by itself prove the prior worker has no effect.
|
|
90
|
-
* Both sides are compared in leaseCompareForm, so a bare and a repository-prefixed spelling of one
|
|
91
|
-
* file overlap and the same relative path in two repositories does not.
|
|
92
|
-
*/
|
|
93
|
-
export function findOwnedPathLeaseConflicts(db,requests,{excludeJobId=null,canonicalOf=null,platform=process.platform}={}){
|
|
94
|
-
const requested=[...new Set(requests.map(item=>item?.resourceKey??item).filter(key=>leasePath(key)!==null))];
|
|
95
|
-
if(!requested.length)return [];
|
|
96
|
-
const held=db.prepare("SELECT resource_key,job_id,workflow_id,op_id,try_no AS attempt,generation,expires_at FROM leases WHERE resource_key LIKE 'path:%' ORDER BY resource_key,job_id").all();
|
|
97
|
-
const formOf=new Map();
|
|
98
|
-
const compare=(key,row)=>{
|
|
99
|
-
const id=`${row?.job_id??''}\0${key}`;
|
|
100
|
-
if(!formOf.has(id))formOf.set(id,leaseCompareForm(leasePath(key),{canonicalOf,row,platform}));
|
|
101
|
-
return formOf.get(id);
|
|
102
|
-
};
|
|
103
|
-
const conflicts=[];
|
|
104
|
-
for(const requestKey of requested){
|
|
105
|
-
const requestPath=compare(requestKey,null);
|
|
106
|
-
for(const row of held){
|
|
107
|
-
if(excludeJobId&&row.job_id===excludeJobId)continue;
|
|
108
|
-
if(ownedPathsIntersect(requestPath,compare(row.resource_key,row)))conflicts.push({requested:requestKey,held:row.resource_key,...row});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return conflicts;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Existing durable path and resource checks, read inside the caller's reservation transaction. */
|
|
115
|
-
export function resourceAdmission(db,repoNeeds,{at,canonicalOf=null}={}){
|
|
116
|
-
const reasons=[];
|
|
117
|
-
const pathConflicts=findOwnedPathLeaseConflicts(db,repoNeeds,{canonicalOf});
|
|
118
|
-
for(const conflict of pathConflicts)reasons.push(`resource ${conflict.requested} overlaps durable lease ${conflict.held} held by ${conflict.job_id}`);
|
|
119
|
-
for(const item of repoNeeds){
|
|
120
|
-
const row=db.prepare('SELECT capacity FROM resources WHERE resource_key=?').get(item.resourceKey);
|
|
121
|
-
const capacity=row?.capacity??1;
|
|
122
|
-
const used=db.prepare('SELECT COALESCE(SUM(units),0) u FROM leases WHERE resource_key=? AND expires_at>?').get(item.resourceKey,at).u;
|
|
123
|
-
if(used+item.units>capacity)reasons.push(`resource ${item.resourceKey} capacity ${capacity} has ${used} used and needs ${item.units}`);
|
|
124
|
-
}
|
|
125
|
-
if(reasons.length)return {ok:false,reason:reasons.join('; '),reasons,pathConflicts:pathConflicts.map(({requested,held,job_id,workflow_id,op_id,expires_at})=>({requested,held,jobId:job_id,workflowId:workflow_id,opId:op_id,expiresAt:expires_at}))};
|
|
126
|
-
return {ok:true};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* The concurrent-operation ceiling one workflow is admitted at. Two declared numbers meet here and
|
|
131
|
-
* the LOWER of them admits: the owner's `budgets.maxOps` (per workflow) and `maxParallelOps` from
|
|
132
|
-
* modules/models/runtimes.yaml (worker-wide). A null, absent or non-positive value is unbounded, so
|
|
133
|
-
* a workflow with no owner budget still meets the worker ceiling. A parallelism gear raises what
|
|
134
|
-
* `starci kernel estimate` requests and never raises either of these.
|
|
135
|
-
*/
|
|
136
|
-
export function opSlotCeiling({maxOps=null,maxParallelOps=null}={}){
|
|
137
|
-
const positive=value=>{const n=Number(value);return Number.isInteger(n)&&n>0?n:null;};
|
|
138
|
-
const owner=positive(maxOps),workers=positive(maxParallelOps);
|
|
139
|
-
if(owner===null&&workers===null)return {ceiling:null,source:null};
|
|
140
|
-
if(owner===null)return {ceiling:workers,source:'maxParallelOps'};
|
|
141
|
-
if(workers===null)return {ceiling:owner,source:'budgets.maxOps'};
|
|
142
|
-
return owner<=workers?{ceiling:owner,source:'budgets.maxOps'}:{ceiling:workers,source:'maxParallelOps'};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Admission against that ceiling. `running` is how many operations of the one workflow already hold
|
|
147
|
-
* a slot; a job at or above the ceiling is refused `max-ops` rather than launched and left to
|
|
148
|
-
* discover the cap from a provider.
|
|
149
|
-
*/
|
|
150
|
-
export function admitOpSlot({running=0,maxOps=null,maxParallelOps=null}={}){
|
|
151
|
-
const {ceiling,source}=opSlotCeiling({maxOps,maxParallelOps});
|
|
152
|
-
const held=Math.max(0,Number(running)||0);
|
|
153
|
-
if(ceiling===null)return {ok:true,running:held,ceiling:null,ceilingSource:null,reason:null};
|
|
154
|
-
return held<ceiling
|
|
155
|
-
?{ok:true,running:held,ceiling,ceilingSource:source,reason:null}
|
|
156
|
-
:{ok:false,running:held,ceiling,ceilingSource:source,reason:'max-ops'};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Count this workflow's durable held slots and judge the same declared ceiling at preflight and reservation. */
|
|
160
|
-
export function workflowOpSlots(db,workflowId,{excludeJobId=null,holdingStatuses,maxOps=null,maxParallelOps=null}={}){
|
|
161
|
-
if(!Array.isArray(holdingStatuses)||!holdingStatuses.length)throw Error('workflowOpSlots requires canonical held statuses');
|
|
162
|
-
const running=db.prepare(`SELECT count(*) n FROM jobs WHERE workflow_id=? AND kind<>'kernel' AND job_id<>?
|
|
163
|
-
AND status IN (${holdingStatuses.map(()=>'?').join(',')})`).get(workflowId,excludeJobId??'',...holdingStatuses).n;
|
|
164
|
-
return admitOpSlot({running,maxOps,maxParallelOps});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const rowObject=value=>{
|
|
168
|
-
if(value&&typeof value==='object')return value;
|
|
169
|
-
if(typeof value!=='string'||!value.trim())return {};
|
|
170
|
-
try{const parsed=JSON.parse(value);return parsed&&typeof parsed==='object'?parsed:{};}catch{return {};}
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* The durable payload/result of a job-shaped row: the parsed object when the row carries the decoded
|
|
175
|
-
* field (a mapped ledger row), else the tolerant parse of its `*_json` text — a missing, blank or
|
|
176
|
-
* unparsable field reads as {}. Several scripts spell this by hand; these are the one pair to cite.
|
|
177
|
-
*/
|
|
178
|
-
export const payloadOf=job=>rowObject(job?.payload??job?.payload_json);
|
|
179
|
-
export const resultOf=job=>rowObject(job?.result??job?.result_json);
|
|
180
|
-
|
|
181
|
-
/** The settled verdict of an attempt that asked the owner and waits for the answer. */
|
|
182
|
-
export const AWAITING_OWNER='awaiting-owner';
|
|
183
|
-
/**
|
|
184
|
-
* jobs.status of a try that ended asking the owner (report outcome ask): settled, but neither a failure nor a spent try
|
|
185
|
-
* (its unit's try budget and business retries ignore it). A retry or resume may follow it exactly as it follows `failed`.
|
|
186
|
-
*/
|
|
187
|
-
export const AWAITING_OWNER_STATUS='awaiting_owner';
|
|
188
|
-
export const RETRYABLE_JOB_STATUSES=Object.freeze(['failed',AWAITING_OWNER_STATUS]);
|
|
189
|
-
/** Every jobs.status that holds nothing the runtime still needs (mirrors engine/db/ledger.mjs JOB_STATUSES.settled). */
|
|
190
|
-
export const SETTLED_JOB_LIST=Object.freeze(['succeeded','failed',AWAITING_OWNER_STATUS,'cancelled']);
|
|
191
|
-
/** The tries of a unit that spent budget: every try but the ones that only waited on the owner. */
|
|
192
|
-
export const spentTries=tries=>tries.filter(job=>job.status!==AWAITING_OWNER_STATUS).length;
|
|
193
|
-
// An attempt the environment killed with effects on the tree (a host terminal wipe: every Orca terminal
|
|
194
|
-
// gone at once, scripts/kernel/cli.mjs hostTerminalWipeOf) settles failed with this retryClass: its retry
|
|
195
|
-
// is a new durable attempt that continues the partial tree and spends no business retry.
|
|
196
|
-
export const RETRY_CLASS_ENVIRONMENT='environment';
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Classify a settled attempt for retry accounting. Infrastructure is free only when the durable result
|
|
200
|
-
* explicitly proves `effectState: none`; unknown or partial effects consume the ordinary business budget.
|
|
201
|
-
* An attempt settled `awaiting-owner` asked a question and did not fail: its successor is a new durable
|
|
202
|
-
* attempt (the ask attempt ran) that spends no business retry. Nor does one settled `peerBlocked`
|
|
203
|
-
* (starci kernel settle: every red check was a peer's change, scripts/kernel/gate-attribution.mjs), nor one settled
|
|
204
|
-
* with retryClass environment (RETRY_CLASS_ENVIRONMENT).
|
|
205
|
-
*/
|
|
206
|
-
export function retryDisposition(job){
|
|
207
|
-
const result=resultOf(job),reason=String(result.reason??'');
|
|
208
|
-
const infrastructure=result.retryClass==='infrastructure'||reason==='dispatch-rejected'||reason==='provider-unavailable';
|
|
209
|
-
const explicitlyReusable=(result.retryable===true&&result.attemptConsumed===false)||result.retryClass==='infrastructure';
|
|
210
|
-
const noEffect=infrastructure&&result.effectState==='none'&&explicitlyReusable;
|
|
211
|
-
const ownerAnswer=!noEffect&&result.verdict===AWAITING_OWNER;
|
|
212
|
-
const peerBlocked=!noEffect&&!ownerAnswer&&result.verdict!=='pass'&&Boolean(result.peerBlocked&&typeof result.peerBlocked==='object');
|
|
213
|
-
const environment=!noEffect&&!ownerAnswer&&!peerBlocked&&result.retryClass===RETRY_CLASS_ENVIRONMENT&&result.attemptConsumed===false;
|
|
214
|
-
return {
|
|
215
|
-
retryClass:noEffect?'infrastructure':ownerAnswer?'owner-answer':peerBlocked?'peer-blocked':environment?RETRY_CLASS_ENVIRONMENT:'business',
|
|
216
|
-
effectState:result.effectState??'unknown',
|
|
217
|
-
resumable:noEffect,
|
|
218
|
-
consumesBusinessRetry:!noEffect&&!ownerAnswer&&!peerBlocked&&!environment,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* A row retired while still queued - `starci kernel reconcile --drop` (result.verdict `dropped`) or a goal revision
|
|
224
|
-
* that superseded it (result.reason `goal-revision-superseded`) - with no dispatch binding in its payload.
|
|
225
|
-
* It ran nothing, so it is no attempt: never a retry predecessor, never a cut seam, never the latest job
|
|
226
|
-
* of its ordinal (inc-5005d003825a: a retry chained to a dropped ordinal-1 row as business attempt 2 and
|
|
227
|
-
* lost the owner-answer lineage; inc-b428eb47fde3: ordinal 2 read a dropped seam as dependency-failed).
|
|
228
|
-
*/
|
|
229
|
-
export function retiredBeforeDispatch(job){
|
|
230
|
-
if(job?.status!=='cancelled')return false;
|
|
231
|
-
const result=resultOf(job),payload=payloadOf(job);
|
|
232
|
-
if(result.verdict!=='dropped'&&result.reason!=='goal-revision-superseded')return false;
|
|
233
|
-
const runtime=payload.hierarchy?.runtime??{};
|
|
234
|
-
const bound=Boolean(job.worker_id||payload.managed||payload.orca||runtime.dispatchId||runtime.terminalHandle
|
|
235
|
-
||(Array.isArray(payload.rejectedDispatches)&&payload.rejectedDispatches.length));
|
|
236
|
-
return !bound;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
/** The cut slice a job row carries, or null: {id, ordinal} identify one bounded SAME-op slice. */
|
|
241
|
-
export function cutOf(job){
|
|
242
|
-
const cut=payloadOf(job).cut;
|
|
243
|
-
return cut&&cut.id!=null&&cut.ordinal!=null?{id:String(cut.id),ordinal:Number(cut.ordinal),total:Number(cut.total)}:null;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
/* ------------------------------------------------------------ work units (H3, H4, H5) */
|
|
248
|
-
|
|
249
|
-
/** The default try budget of a unit (Q13; DBTREE work_units.try_budget). Only the owner or the Supervisor raises one. */
|
|
250
|
-
export const UNIT_TRY_BUDGET=5;
|
|
251
|
-
const shortDigest=value=>createHash('sha256').update(value).digest('hex').slice(0,16);
|
|
252
|
-
const lineagePaths=list=>(Array.isArray(list)?list:[]).map(item=>typeof item==='string'?item:item?.path).filter(p=>typeof p==='string'&&p.trim());
|
|
253
|
-
const normList=list=>[...new Set(lineagePaths(list).map(p=>p.replace(/\\/g,'/').replace(/\/\*\*$/,'').replace(/\/+$/,'')))].sort();
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* The work identity of a job (DBTREE work_units.subject_key): a cut slice is `cut:<id>#<ordinal>`, an op about one
|
|
257
|
-
* named subject `subject:<s>`, else the digest of its records, else of its owned paths. One op, one subject key and
|
|
258
|
-
* one goal revision are ONE unit (UNIQUE(workflow_id, op_id, subject_key, goal_revision)): a try of the same work can
|
|
259
|
-
* never start a fresh budget.
|
|
260
|
-
*/
|
|
261
|
-
export function unitSubjectKey({cut=null,params=null,records=[],ownedPaths=[]}={}){
|
|
262
|
-
if(cut?.id!=null&&cut?.ordinal!=null)return `cut:${cut.id}#${Number(cut.ordinal)}`;
|
|
263
|
-
const subject=typeof params?.subject==='string'&¶ms.subject.trim()?params.subject.trim():null;
|
|
264
|
-
if(subject)return `subject:${subject}`;
|
|
265
|
-
const recs=normList(records);
|
|
266
|
-
if(recs.length)return `records:${shortDigest(recs.join('|'))}`;
|
|
267
|
-
return `paths:${shortDigest(normList(ownedPaths).join('|'))}`;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/** Two jobs are tries of one work unit (jobs.unit_id). */
|
|
271
|
-
export const sameUnit=(a,b)=>Boolean(a?.unit_id&&a.unit_id===b?.unit_id);
|
|
272
|
-
|
|
273
|
-
const OPEN_TRY=['queued','ready','leased','running','answering','reported','deciding','effect_unknown'];
|
|
274
|
-
|
|
275
|
-
/** The jobs.retry_class of a successor of `last` (DBTREE: business | infra | resume | follow-up). */
|
|
276
|
-
const retryClassOf=(last,disposition)=>last.status==='cancelled'?'resume'
|
|
277
|
-
:disposition?.retryClass==='business'?'business'
|
|
278
|
-
:disposition?.retryClass==='infrastructure'||disposition?.retryClass===RETRY_CLASS_ENVIRONMENT?'infra':'follow-up';
|
|
279
|
-
/**
|
|
280
|
-
* Admit one more try of a unit (the code side of DBTREE jobs_enqueue_guard + work_units_done_guard), pure over the
|
|
281
|
-
* unit's row and its tries. `tries` are the unit's jobs with {job_id, status, try_no, result_json?} (result_json the
|
|
282
|
-
* settle result retryDisposition reads); `unit` is the work_units row. Returns {tryNo, retryOf, resumeOf, retryClass,
|
|
283
|
-
* reopen} or throws a typed refusal:
|
|
284
|
-
* unit-in-flight a try of the unit is still open (edit it, or let it settle first)
|
|
285
|
-
* unit-already-passed the unit is done; a re-run needs an explicit reopen with a reason (H5)
|
|
286
|
-
* retry-lineage-invalid retryOf is not the unit's latest try, or that try did not fail (H4)
|
|
287
|
-
* unit-try-budget-exhausted try_no would pass work_units.try_budget; only the owner or the Supervisor raises it (H3)
|
|
288
|
-
*/
|
|
289
|
-
export function admitUnitTry({unit=null,tries=[],retryOf=null,reopen=null}={}){
|
|
290
|
-
const ordered=[...tries].sort((a,b)=>Number(a.try_no)-Number(b.try_no));
|
|
291
|
-
const last=ordered.at(-1)??null;
|
|
292
|
-
if(!unit||!last){
|
|
293
|
-
if(retryOf)throw refuse(`--retry-of ${retryOf} names no earlier try of this unit`,'retry-lineage-invalid');
|
|
294
|
-
return {tryNo:1,retryOf:null,resumeOf:null,retryClass:null,reopen:null};
|
|
295
|
-
}
|
|
296
|
-
const open=ordered.filter(job=>OPEN_TRY.includes(job.status));
|
|
297
|
-
if(open.length)throw refuse(`unit ${unit.unit_id} already has an open try ${open.map(j=>`${j.job_id} (${j.status})`).join(', ')}: edit that try (starci kernel graph-edit widen|params) or let it settle`,'unit-in-flight',{open:open.map(j=>j.job_id)});
|
|
298
|
-
if(retryOf&&retryOf!==last.job_id)throw refuse(`--retry-of ${retryOf} is not the latest try of unit ${unit.unit_id} (${last.job_id} is): a retry follows the unit's latest failed try`,'retry-lineage-invalid',{latest:last.job_id});
|
|
299
|
-
const done=unit.state==='done'||last.status==='succeeded';
|
|
300
|
-
if(done&&!(reopen?.reason&&reopen?.by))throw refuse(`unit ${unit.unit_id} already passed (${last.job_id}); running it again needs an explicit reopen with a reason (--reopen <reason>)`,'unit-already-passed',{passed:last.job_id});
|
|
301
|
-
if(retryOf&&!done&&!RETRYABLE_JOB_STATUSES.includes(last.status))throw refuse(`--retry-of ${retryOf} is ${last.status}: a retry follows a FAILED or awaiting_owner try of the same unit`,'retry-lineage-invalid');
|
|
302
|
-
const tryNo=Number(last.try_no)+1;
|
|
303
|
-
if(tryNo-(ordered.length-spentTries(ordered))>Number(unit.try_budget))throw refuse(`unit ${unit.unit_id} spent ${spentTries(ordered)} of its ${unit.try_budget} tries: the owner or the Supervisor decides (starci kernel unit --raise-budget), never another try`,'unit-try-budget-exhausted',{tries:Number(last.try_no),budget:Number(unit.try_budget)});
|
|
304
|
-
if(done)return {tryNo,retryOf:null,resumeOf:null,retryClass:'follow-up',reopen:{reason:String(reopen.reason),by:String(reopen.by)}};
|
|
305
|
-
const disposition=RETRYABLE_JOB_STATUSES.includes(last.status)?retryDisposition(last):null;
|
|
306
|
-
const resume=last.status==='cancelled';
|
|
307
|
-
return {tryNo,retryOf:resume?null:last.job_id,resumeOf:resume?last.job_id:null,retryClass:retryClassOf(last,disposition),reopen:null};
|
|
308
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// canonical-json.mjs — the one canonical JSON the runtime digests structured values with: object keys
|
|
2
|
-
// sorted, arrays in order, so two equal values always serialize to the same bytes. A leaf beside digest.mjs.
|
|
3
|
-
import {isPlainObject} from './plain-object.mjs';
|
|
4
|
-
|
|
5
|
-
/** The canonical JSON text of `value`: keys sorted at every depth. */
|
|
6
|
-
export function canonicalJSON(value) {
|
|
7
|
-
if (Array.isArray(value)) return `[${value.map(canonicalJSON).join(',')}]`;
|
|
8
|
-
if (isPlainObject(value)) return `{${Object.keys(value).sort().map(k => `${JSON.stringify(k)}:${canonicalJSON(value[k])}`).join(',')}}`;
|
|
9
|
-
return JSON.stringify(value);
|
|
10
|
-
}
|