@starci/hfs 2.0.2 → 4.0.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 +25 -0
- package/README.md +33 -33
- package/bin/hfs.mjs +94 -68
- package/lint/run.mjs +161 -0
- package/package.json +3 -2
- package/report/sonar.mjs +14 -29
- package/runtime/engine/admission.mjs +3 -3
- package/runtime/engine/ledger-db.mjs +2 -2
- package/runtime/engine/machine-db.mjs +90 -9
- package/runtime/engine/migrations/machine/0002-worktrees-no-workflow-kind.sql +13 -0
- package/runtime/engine/migrations/runtime/0005-ended-workflow-views.sql +93 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +32 -13
- package/runtime/knowledge/hfs/peer-integrations.yaml +18 -0
- package/runtime/knowledge/hfs/slots.yaml +193 -128
- package/runtime/knowledge/patterns/fe/folder.yaml +36 -36
- package/runtime/knowledge/sonar-gate.yaml +8 -7
- package/runtime/modules/kernel/failure-codes.yaml +31 -52
- package/runtime/scripts/checks/architecture/backend.mjs +1 -1
- package/runtime/scripts/checks/architecture/config.mjs +31 -11
- package/runtime/scripts/checks/architecture/contracts.mjs +4 -4
- package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +7 -3
- package/runtime/scripts/checks/architecture/framework-pinned.mjs +5 -47
- package/runtime/scripts/checks/architecture/frontend.mjs +6 -4
- package/runtime/scripts/checks/architecture/hfs-graph.mjs +1 -1
- package/runtime/scripts/checks/architecture/hfs.mjs +105 -67
- package/runtime/scripts/checks/architecture/index.mjs +18 -13
- package/runtime/scripts/checks/architecture/next-data.mjs +3 -2
- package/runtime/scripts/checks/architecture/owners.mjs +12 -6
- package/runtime/scripts/checks/architecture/registration.mjs +1 -1
- package/runtime/scripts/checks/architecture/surface.mjs +91 -0
- package/runtime/scripts/checks/architecture/symbols.mjs +13 -2
- package/runtime/scripts/checks/architecture/test-world-files.mjs +83 -45
- package/runtime/scripts/checks/architecture/typescript.mjs +127 -29
- package/runtime/scripts/checks/typescript-programs.mjs +2 -2
- package/runtime/scripts/lib/hfs-check.mjs +160 -209
- package/runtime/scripts/lib/hfs-path-findings.mjs +95 -0
- package/runtime/scripts/lib/hfs-rules/contract.mjs +15 -42
- package/runtime/scripts/lib/hfs-rules/frontend.mjs +37 -36
- package/runtime/scripts/lib/hfs-rules/peer-integrations.mjs +44 -0
- package/runtime/scripts/lib/hfs-rules/pipeline.mjs +12 -13
- package/runtime/scripts/lib/hfs-rules/proof-commands.mjs +11 -8
- package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +1 -3
- package/runtime/scripts/lib/hfs-slots.mjs +244 -61
- package/runtime/scripts/lib/hfs-view.mjs +9 -7
- package/runtime/scripts/lib/language.mjs +11 -1
- package/runtime/scripts/lib/safe-remove.mjs +95 -10
- package/scaffold/app.mjs +179 -0
- package/scaffold/service.mjs +26 -16
- package/sync/cli.mjs +1 -1
- package/sync/hygiene.mjs +11 -8
- package/sync/index.mjs +109 -111
- package/sync/managed.mjs +9 -8
- package/sync/sonar-key.mjs +20 -22
- package/templates/{be → app}/ci-workflows/github/workflows/ci.yml +5 -11
- package/templates/app/gitignore +6 -0
- package/templates/app/hooks/husky/pre-commit +25 -0
- package/templates/app/hooks/husky/pre-push +7 -0
- package/templates/app/package-scripts/package.json +22 -0
- package/templates/{be → app}/quality-config/sonar-project.properties +4 -3
- package/templates/app/skeleton/.editorconfig +15 -0
- package/templates/app/skeleton/.gitattributes +2 -0
- package/templates/app/skeleton/.nvmrc +1 -0
- package/templates/app/skeleton/.starciwork/features/index.yaml +7 -0
- package/templates/app/skeleton/.starciwork/workspace.yaml +9 -0
- package/templates/app/skeleton/README.md +36 -0
- package/templates/app/skeleton/scripts/codegen.mjs +4 -0
- package/templates/{fe → app}/tool-config/prettierignore +4 -1
- package/templates/be/skeleton/.sops.yaml +2 -0
- package/templates/be/skeleton/.starcistacks/application-stacks.yaml +10 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +3 -0
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +27 -6
- package/templates/be/skeleton/apps/__app__/src/main.ts +4 -1
- package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +11 -0
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +25 -0
- package/templates/be/skeleton/src/modules/domain/identity/errors/identity.error.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +11 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +14 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/identity/index.ts +6 -0
- package/templates/be/skeleton/src/modules/domain/identity/messages/identity.messages.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +8 -1
- package/templates/be/skeleton/src/modules/platform/composition/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +71 -23
- package/templates/be/skeleton/src/modules/platform/config/errors/config.error.ts +14 -16
- package/templates/be/skeleton/src/modules/platform/config/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/config/server.config.ts +2 -12
- package/templates/be/skeleton/src/modules/platform/errors/domain.error.ts +16 -7
- package/templates/be/skeleton/src/modules/platform/errors/errors/errors.error.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.contracts.ts +33 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.decorators.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.ts +32 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.log-events.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.options.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.service.spec.ts +94 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.service.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/errors/http-status.policy.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/errors/index.ts +4 -1
- package/templates/be/skeleton/src/modules/platform/errors/messages/errors.messages.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.decorators.ts +10 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +17 -0
- package/templates/be/skeleton/src/modules/platform/http-security/index.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +31 -0
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.ts +68 -0
- package/templates/be/skeleton/src/modules/platform/i18n/bundle-message-catalog.service.spec.ts +66 -0
- package/templates/be/skeleton/src/modules/platform/i18n/bundle-message-catalog.service.ts +30 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.contracts.ts +18 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.decorators.ts +23 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.module.ts +24 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.options.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.port.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/i18n/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/i18n/request-locale.service.spec.ts +45 -0
- package/templates/be/skeleton/src/modules/platform/i18n/request-locale.service.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/logging/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +85 -68
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +14 -10
- package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.port.ts +0 -1
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.contracts.ts +25 -0
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.ts +24 -0
- package/templates/fe/skeleton/apps/__app__/postcss.config.mjs +7 -0
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/error.tsx +5 -17
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +11 -22
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/loading.tsx +6 -0
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +3 -12
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/page.tsx +6 -24
- package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +4 -18
- package/templates/fe/skeleton/apps/__app__/src/app/globals.css +5 -0
- package/templates/fe/skeleton/apps/__app__/src/components/composites/FailureScreen/index.tsx +28 -0
- package/templates/fe/skeleton/apps/__app__/src/features/layouts/LocaleShell/index.tsx +35 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +21 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +17 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +26 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +15 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +25 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +15 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +27 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +32 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/config/index.ts +12 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/index.ts +2 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages/vi.json +6 -0
- package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/request.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/routes/index.ts +4 -0
- package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/proxy.ts +1 -1
- package/runtime/scripts/checks/architecture/size-growth.mjs +0 -73
- package/sync/skeleton.mjs +0 -76
- package/templates/be/gitignore +0 -2
- package/templates/be/hooks/husky/pre-commit +0 -13
- package/templates/be/hooks/husky/pre-push +0 -7
- package/templates/be/package-scripts/package.json +0 -22
- package/templates/be/skeleton/scripts/.gitkeep +0 -0
- package/templates/be/skeleton/src/modules/platform/config/config.decorators.ts +0 -9
- package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +0 -20
- package/templates/be/tool-config/prettierignore +0 -8
- package/templates/fe/ci-workflows/github/workflows/ci.yml +0 -54
- package/templates/fe/gitignore +0 -3
- package/templates/fe/hooks/husky/pre-commit +0 -16
- package/templates/fe/hooks/husky/pre-push +0 -6
- package/templates/fe/package-scripts/package.json +0 -17
- package/templates/fe/parts/api-client.ts +0 -44
- package/templates/fe/parts/api-outcome.ts +0 -7
- package/templates/fe/quality-config/sonar-project.properties +0 -8
- package/templates/fe/skeleton/scripts/.gitkeep +0 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/client.ts +0 -1
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/index.ts +0 -3
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/outcome.ts +0 -1
- package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/index.ts +0 -4
- package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/navigation.ts +0 -5
- package/templates/fe/skeleton-shared/apps/__app__/next.config.ts +0 -12
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/api/index.ts +0 -2
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/index.ts +0 -9
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/request.ts +0 -5
- package/templates/fe/skeleton-shared/apps/__app__/src/proxy.ts +0 -5
- package/templates/fe/skeleton-shared/packages/__family__-api/package.json +0 -12
- package/templates/fe/skeleton-shared/packages/__family__-api/src/client.ts +0 -1
- package/templates/fe/skeleton-shared/packages/__family__-api/src/index.ts +0 -3
- package/templates/fe/skeleton-shared/packages/__family__-api/src/outcome.ts +0 -1
- package/templates/fe/skeleton-shared/packages/__family__-api/tsconfig.json +0 -5
- package/templates/fe/skeleton-shared/packages/__family__-i18n/package.json +0 -18
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/app.ts +0 -19
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/index.ts +0 -2
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/proxy.ts +0 -12
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/request.ts +0 -15
- package/templates/fe/skeleton-shared/packages/__family__-i18n/tsconfig.json +0 -5
- package/templates/fe/tool-config/prettierrc +0 -1
- /package/templates/{be → app}/ci-workflows/github/workflows/e2e.yml +0 -0
- /package/templates/{be → app}/starciwork.gitignore +0 -0
- /package/templates/{be → app}/tool-config/prettierrc +0 -0
- /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/next.config.ts +0 -0
- /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/config.ts +0 -0
- /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/routing.ts +0 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/hfs",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "The HFS command line of a StarCi
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "The HFS command line of a StarCi app (one repository: the root, be/ and fe/): hfs lint, check, scaffold app, explain, sync and work-hygiene. Self-contained: it carries the runtime files it reads.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"private": false,
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"bin/hfs.mjs",
|
|
17
17
|
"emit/**",
|
|
18
|
+
"lint/**",
|
|
18
19
|
"scaffold/**",
|
|
19
20
|
"report/**",
|
|
20
21
|
"runtime/**",
|
package/report/sonar.mjs
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// into the same document format, engineId `eslint` / `stylelint`, rule id = the linter's
|
|
9
|
-
// rule. Sonar's own ESLint import (sonar.eslint.reportPaths) is not used: it drops an issue
|
|
10
|
-
// on a file outside sonar.sources, and a stylelint result has no native import at all.
|
|
1
|
+
// Sonar documents of the ONE lint entry `hfs lint` (contract changes hfs-sonar-import, hfs-lint-entry): the building blocks that turn the
|
|
2
|
+
// findings of the StarCi canon into a Sonar Generic Issue Import document (SonarQube 10.3+ format: `{ rules, issues }`).
|
|
3
|
+
// sonarReport(findings) the repository findings of `hfs check`, engineId `starci-hfs`, rule id = the finding code
|
|
4
|
+
// linterReport(kind, results) a linter's own json output (`eslint -f json`, `stylelint --formatter json`), engineId `eslint` / `stylelint`,
|
|
5
|
+
// rule id = the linter's rule. Sonar's own ESLint import (sonar.eslint.reportPaths) is not used: it drops an
|
|
6
|
+
// issue on a file outside sonar.sources, and a stylelint result has no native import at all.
|
|
7
|
+
// mergeReports(reports) the documents of one `hfs lint` run as ONE file (reports/lint.sonar.json, sonar.externalIssuesReportPaths)
|
|
11
8
|
// One placement rule for every engine: Sonar imports an issue only on a file it indexes (a tracked source or stylesheet under
|
|
12
9
|
// sonar.sources). A finding on any other path (hfs.json, a workflow, a package under an unindexed root, e2e/, a directory) is filed
|
|
13
10
|
// on the first source file of sonar.sources and its message names the real path, so no finding is dropped.
|
|
@@ -42,6 +39,12 @@ function document(rules, issues) {
|
|
|
42
39
|
return { rules: [...rules.values()].filter((rule) => used.has(rule.id)).sort((a, b) => cmp(a.id, b.id)), issues: sorted };
|
|
43
40
|
}
|
|
44
41
|
|
|
42
|
+
/** One document of several (the starci-hfs, eslint and stylelint documents of one `hfs lint` run): rules by id, issues in the one sort order. */
|
|
43
|
+
export function mergeReports(reports) {
|
|
44
|
+
const rules = new Map(reports.flatMap((report) => report.rules).map((rule) => [rule.id, rule]));
|
|
45
|
+
return document(rules, reports.flatMap((report) => report.issues));
|
|
46
|
+
}
|
|
47
|
+
|
|
45
48
|
/**
|
|
46
49
|
* The first source file of the repository (sorted) under one of `sourceRoots`: Sonar imports an issue only on a file it
|
|
47
50
|
* indexes, and a finding about hfs.json, a workflow, a config file or a directory is not on one, so it is filed on this
|
|
@@ -106,7 +109,6 @@ const LINTERS = Object.freeze({
|
|
|
106
109
|
text: (message) => message.message,
|
|
107
110
|
describe: (id) => `ESLint rule ${id} of the StarCi canon (@starci/eslint-canon-be and @starci/eslint-canon-fe) or of a plugin it composes.`,
|
|
108
111
|
describeError: 'ESLint could not lint a file: a parse error or an invalid configuration.',
|
|
109
|
-
shape: 'an array of results with a filePath and messages',
|
|
110
112
|
},
|
|
111
113
|
stylelint: {
|
|
112
114
|
engine: STYLELINT_ENGINE,
|
|
@@ -117,11 +119,10 @@ const LINTERS = Object.freeze({
|
|
|
117
119
|
text: stylelintText,
|
|
118
120
|
describe: (id) => `Stylelint rule ${id} of the StarCi CSS canon (@starci/stylelint-canon, HFS R61).`,
|
|
119
121
|
describeError: 'Stylelint could not read a stylesheet: a parse error or an invalid option.',
|
|
120
|
-
shape: 'an array of results with a source and warnings',
|
|
121
122
|
},
|
|
122
123
|
});
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
const LINTER_KINDS = Object.freeze(Object.keys(LINTERS));
|
|
125
126
|
|
|
126
127
|
/**
|
|
127
128
|
* A linter's json results (`eslint -f json`: [{ filePath, messages: [{ ruleId, line, endLine, message }] }]; `stylelint
|
|
@@ -162,19 +163,3 @@ export function writeReport(file, report) {
|
|
|
162
163
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
163
164
|
fs.writeFileSync(file, `${JSON.stringify(report, null, 2)}\n`);
|
|
164
165
|
}
|
|
165
|
-
|
|
166
|
-
/** `hfs report <eslint|stylelint> <in> <out> [--repo <dir>]`: convert a linter's json report; returns the number of issues written. */
|
|
167
|
-
export function convertReportFile({ kind, input, output, root, sourceRoots = [], tracked = [] }) {
|
|
168
|
-
const linter = LINTERS[kind];
|
|
169
|
-
if (linter === undefined) throw new Error(`unknown linter ${kind}; expected ${LINTER_KINDS.join(' or ')}`);
|
|
170
|
-
let results;
|
|
171
|
-
try {
|
|
172
|
-
results = JSON.parse(fs.readFileSync(input, 'utf8'));
|
|
173
|
-
} catch (error) {
|
|
174
|
-
throw new Error(`${input} is not a readable ${kind} json report (${error.message})`);
|
|
175
|
-
}
|
|
176
|
-
if (!Array.isArray(results)) throw new Error(`${input} is not a ${kind} json report: expected ${linter.shape}`);
|
|
177
|
-
const report = linterReport(kind, results, { root, sourceRoots, tracked });
|
|
178
|
-
writeReport(output, report);
|
|
179
|
-
return report.issues.length;
|
|
180
|
-
}
|
|
@@ -72,9 +72,9 @@ const leasePath=resourceKey=>String(resourceKey??'').startsWith(PATH_LEASE_PREFI
|
|
|
72
72
|
* The spelling two lease paths are compared in. The same file must compare equal however a workflow
|
|
73
73
|
* spelled it (nivo wf-nivo-fe-debt-mug06w7h inc-52a4a5ee5b12: `apps/app/src/messages/vi.json` bare for
|
|
74
74
|
* the fe repository vs `nivo-fe/apps/app/src/messages` repository-prefixed never overlapped). `canonicalOf`
|
|
75
|
-
* (scripts/kernel/lease-canon.mjs) resolves a path to its
|
|
76
|
-
*
|
|
77
|
-
*
|
|
75
|
+
* (scripts/kernel/lease-canon.mjs) resolves a path to its app-relative form in a bound app
|
|
76
|
+
* (be/<path>, fe/<path>) — for a held row through its holder job, so every spelling of one file
|
|
77
|
+
* compares as one key; paths on Windows compare case-insensitively, as its file
|
|
78
78
|
* systems do.
|
|
79
79
|
*/
|
|
80
80
|
export const leaseCompareForm=(leasePathValue,{canonicalOf=null,row=null,platform=process.platform}={})=>{
|
|
@@ -54,7 +54,7 @@ export const JOB_ARTIFACT_ROLES=Object.freeze(['check-output','check-stdout','ch
|
|
|
54
54
|
'direction','prompt','render','redline','critique','capture','dom','screenshot','video','trace','uat-run','metrics','salvage','scan','other']);
|
|
55
55
|
|
|
56
56
|
export const LEDGER_SCHEMA='starci/runtime@1';
|
|
57
|
-
export const LEDGER_VERSION=
|
|
57
|
+
export const LEDGER_VERSION=5;
|
|
58
58
|
|
|
59
59
|
const need=(ok,message,code)=>{if(!ok)throw Object.assign(Error(message),code?{code}:{});};
|
|
60
60
|
const json=value=>value===undefined||value===null?null:JSON.stringify(value);
|
|
@@ -127,7 +127,7 @@ const INIT_SQL_FILE=new URL('./migrations/runtime/0001-init.sql',import.meta.url
|
|
|
127
127
|
const INIT_SQL=fs.readFileSync(INIT_SQL_FILE,'utf8');
|
|
128
128
|
const INIT_SQL_SHA=sha256(INIT_SQL);
|
|
129
129
|
/** Forward migrations after 0001-init, in order; each bumps user_version to its `version` (migrateLedger). */
|
|
130
|
-
const FORWARD_MIGRATIONS=Object.freeze([{version:3,name:'0003-usage-unavailable'},{version:4,name:'0004-attempt-why'}]);
|
|
130
|
+
const FORWARD_MIGRATIONS=Object.freeze([{version:3,name:'0003-usage-unavailable'},{version:4,name:'0004-attempt-why'},{version:5,name:'0005-ended-workflow-views'}]);
|
|
131
131
|
export const LEDGER_BUSY_TIMEOUT_MS=15000;
|
|
132
132
|
/**
|
|
133
133
|
* Writer pragmas. wal_autocheckpoint=0 on EVERY connection except the one checkpointer (openLedger({checkpointer:true}),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// engine/machine-db.mjs — the ONE writer of machine.sqlite (DBTREE.sql Part B, schema 'starci/machine@1', user_version
|
|
1
|
+
// engine/machine-db.mjs — the ONE writer of machine.sqlite (DBTREE.sql Part B, schema 'starci/machine@1', user_version MACHINE_VERSION).
|
|
2
2
|
//
|
|
3
3
|
// machine.sqlite is the host's single operational store: the ledger registry, the Supervisor (sup_*), the engine
|
|
4
4
|
// reconciler (process_runs, engine_leader, leader_history, schedules, engine_actions, controller_modes, sla_episodes),
|
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
// and the next flushOutbox (the land gate) applies it. Only idempotent writers are deferrable (DEFERRABLE).
|
|
25
25
|
// reader : readOnly, query_only=ON, busy_timeout=15000
|
|
26
26
|
// startup : sqlite_version, node_version, journal_mode and user_version are recorded in machine_meta; an old-schema file
|
|
27
|
-
// (anything that is not 'starci/machine@1') is refused
|
|
28
|
-
//
|
|
27
|
+
// (anything that is not 'starci/machine@1') is refused — a fresh machine.sqlite is created by openMachine on
|
|
28
|
+
// first use. A starci/machine@1 file at an older user_version is brought forward by migrateMachine
|
|
29
|
+
// (engine/migrations/machine/000N-*.sql after 0001-init) on the first writer open.
|
|
29
30
|
// Nothing outside engine/ opens machine.sqlite with `new DatabaseSync`: callers use openMachine / openMachineReader /
|
|
30
31
|
// withMachine / readMachine and the typed functions on the handle.
|
|
31
32
|
import fs from 'node:fs';
|
|
@@ -42,7 +43,9 @@ import { redactBytes, redactData, redactText } from '../scripts/lib/redact.mjs';
|
|
|
42
43
|
const require = createRequire(import.meta.url);
|
|
43
44
|
const ENGINE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
44
45
|
export const MACHINE_SCHEMA = 'starci/machine@1';
|
|
45
|
-
export const MACHINE_VERSION =
|
|
46
|
+
export const MACHINE_VERSION = 2;
|
|
47
|
+
/** Forward migrations after 0001-init, in order; each bumps user_version to its `version` (migrateMachine). */
|
|
48
|
+
export const MACHINE_MIGRATIONS = Object.freeze([{ version: 2, name: '0002-worktrees-no-workflow-kind' }]);
|
|
46
49
|
export const MACHINE_BUSY_TIMEOUT_MS = 15000;
|
|
47
50
|
/** Test seam: STARCI_MACHINE_BUSY_TIMEOUT_MS (a positive integer) replaces the writer's busy_timeout; unset in production. */
|
|
48
51
|
export const busyTimeoutOf = (env = process.env) => { const n = Number(env?.STARCI_MACHINE_BUSY_TIMEOUT_MS); return Number.isInteger(n) && n > 0 ? n : MACHINE_BUSY_TIMEOUT_MS; };
|
|
@@ -256,7 +259,7 @@ export const compareRevs = (a, b) => { const t = (r) => (/^\d{13}:/.test(String(
|
|
|
256
259
|
|
|
257
260
|
/** An old or foreign store: refuse it; a fresh store is created by openMachine on first use. */
|
|
258
261
|
function refuseOld(file, why) {
|
|
259
|
-
throw Object.assign(Error(`machine-schema-old: ${path.resolve(file)} ${why}; this runtime opens only '${MACHINE_SCHEMA}' (user_version
|
|
262
|
+
throw Object.assign(Error(`machine-schema-old: ${path.resolve(file)} ${why}; this runtime opens only '${MACHINE_SCHEMA}' (user_version 1..${MACHINE_VERSION}) — move the refused file aside and openMachine creates a fresh machine.sqlite on first use`),
|
|
260
263
|
{ code: 'STARCI_MACHINE_SCHEMA_OLD' });
|
|
261
264
|
}
|
|
262
265
|
|
|
@@ -267,7 +270,51 @@ function checkSchema(db, file) {
|
|
|
267
270
|
const schema = db.prepare("SELECT value FROM machine_meta WHERE key='schema'").get()?.value;
|
|
268
271
|
if (schema !== MACHINE_SCHEMA) refuseOld(file, `is schema '${schema ?? 'none'}'`);
|
|
269
272
|
if (version > MACHINE_VERSION) throw Object.assign(Error(`machine-schema-newer: ${file} user_version ${version} > ${MACHINE_VERSION}; upgrade the runtime`), { code: 'STARCI_MACHINE_SCHEMA_NEWER' });
|
|
270
|
-
|
|
273
|
+
// 1..MACHINE_VERSION: an older version is brought forward on the first writer open (migrateMachine); a reader reads it as is.
|
|
274
|
+
if (!(version >= 1)) refuseOld(file, `is user_version ${version}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Bring an older machine.sqlite forward: for each MACHINE_MIGRATIONS entry above user_version and not yet in schema_migrations -
|
|
279
|
+
* integrity_check and a VACUUM INTO backup (`<file>.pre-<name>.bak`, skipped for a store created by this open) first, then in one
|
|
280
|
+
* transaction the migration SQL, schema_version+1, user_version, its schema_migrations row, foreign_key_check and quick_check
|
|
281
|
+
* before COMMIT. Idempotent and safe across processes (re-checked under the lock). Returns the names applied.
|
|
282
|
+
*/
|
|
283
|
+
export function migrateMachine(db, { file, now = Date.now, backup: wantBackup = true } = {}) {
|
|
284
|
+
const version = () => Number(pragma(db, 'user_version'));
|
|
285
|
+
const pending = () => MACHINE_MIGRATIONS.filter((m) => version() < m.version && !db.prepare('SELECT 1 FROM schema_migrations WHERE version=?').get(m.version));
|
|
286
|
+
if (!pending().length) return [];
|
|
287
|
+
const check = db.prepare('PRAGMA integrity_check').all();
|
|
288
|
+
need(check.length === 1 && check[0].integrity_check === 'ok', `machine-migrate-refused: ${file} fails integrity_check`, 'STARCI_MACHINE_MIGRATE_REFUSED');
|
|
289
|
+
const applied = [];
|
|
290
|
+
for (const m of pending()) {
|
|
291
|
+
const sql = fs.readFileSync(path.join(ENGINE_DIR, 'migrations', 'machine', `${m.name}.sql`), 'utf8');
|
|
292
|
+
const backup = wantBackup && file ? `${path.resolve(file)}.pre-${m.name}.bak` : null;
|
|
293
|
+
if (backup && !fs.existsSync(backup)) db.exec(`VACUUM INTO '${backup.replace(/'/g, "''")}'`);
|
|
294
|
+
db.exec('BEGIN IMMEDIATE');
|
|
295
|
+
let relaxed = false;
|
|
296
|
+
try {
|
|
297
|
+
if (version() >= m.version) { db.exec('ROLLBACK'); continue; }
|
|
298
|
+
if (typeof db.enableDefensive === 'function') { db.enableDefensive(false); relaxed = true; } // writable_schema is refused while defensive
|
|
299
|
+
const started = now();
|
|
300
|
+
db.exec(sql);
|
|
301
|
+
db.exec(`PRAGMA schema_version=${Number(pragma(db, 'schema_version')) + 1}`);
|
|
302
|
+
db.exec('PRAGMA writable_schema=OFF');
|
|
303
|
+
db.exec(`PRAGMA user_version=${m.version}`);
|
|
304
|
+
db.prepare("INSERT INTO schema_migrations(version,name,runtime_rev,sql_sha256,backup_path,started_at,finished_at,status) VALUES(?,?,?,?,?,?,?,'done')")
|
|
305
|
+
.run(m.version, m.name, runtimeRev(), sha256(sql), backup, started, now());
|
|
306
|
+
need(db.prepare('PRAGMA foreign_key_check').all().length === 0, `machine-migrate-failed: ${m.name} left a foreign key violation`, 'STARCI_MACHINE_MIGRATE_FAILED');
|
|
307
|
+
const quick = db.prepare('PRAGMA quick_check').all();
|
|
308
|
+
need(quick.length === 1 && quick[0].quick_check === 'ok', `machine-migrate-failed: ${m.name} fails quick_check`, 'STARCI_MACHINE_MIGRATE_FAILED');
|
|
309
|
+
db.exec('COMMIT');
|
|
310
|
+
applied.push(m.name);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
try { db.exec('ROLLBACK'); } catch { /* none */ }
|
|
313
|
+
try { db.exec('PRAGMA writable_schema=OFF'); } catch { /* none */ }
|
|
314
|
+
throw error;
|
|
315
|
+
} finally { if (relaxed) db.enableDefensive(true); }
|
|
316
|
+
}
|
|
317
|
+
return applied;
|
|
271
318
|
}
|
|
272
319
|
|
|
273
320
|
function createSchema(db, { file, env, now }) {
|
|
@@ -288,7 +335,7 @@ function createSchema(db, { file, env, now }) {
|
|
|
288
335
|
db.prepare("INSERT INTO mode_changes(controller,from_mode,to_mode,by,reason,at) VALUES(?,NULL,'shadow','machine-db:init','0001-init',?)").run(controller, at);
|
|
289
336
|
db.prepare("INSERT INTO controller_modes(controller,mode,set_at,set_by) VALUES(?,'shadow',?,'machine-db:init')").run(controller, at);
|
|
290
337
|
}
|
|
291
|
-
db.exec(
|
|
338
|
+
db.exec('PRAGMA user_version=1'); // 0001; the forward files follow in migrateMachine
|
|
292
339
|
}
|
|
293
340
|
db.exec('COMMIT');
|
|
294
341
|
} catch (error) { try { db.exec('ROLLBACK'); } catch { /* none */ } throw error; }
|
|
@@ -327,6 +374,7 @@ function openConnection(file, { readOnly = false, env = process.env, now = Date.
|
|
|
327
374
|
createSchema(db, { file, env, now });
|
|
328
375
|
}
|
|
329
376
|
checkSchema(db, file);
|
|
377
|
+
migrateMachine(db, { file, now, backup: !fresh });
|
|
330
378
|
recordFacts(db);
|
|
331
379
|
return db;
|
|
332
380
|
} catch (error) {
|
|
@@ -1254,6 +1302,39 @@ const pushes = (m, { repoRoot = null, limit = 50 } = {}) => m.db.prepare(`SELECT
|
|
|
1254
1302
|
const upsertWorktree = (m, wt) => upsertRow(m.db, 'worktrees', { created_at: m.now(), ...snake(wt), path: path.resolve(wt.path) }, ['path']);
|
|
1255
1303
|
const removedWorktree = (m, wtPath, { error = null, archivedRef = null } = {}) => m.db.prepare('UPDATE worktrees SET removed_at=CASE WHEN ? IS NULL THEN ? ELSE removed_at END, remove_error=?, archived_ref=COALESCE(?,archived_ref) WHERE path=?')
|
|
1256
1304
|
.run(error, m.now(), error, archivedRef, path.resolve(wtPath)).changes > 0;
|
|
1305
|
+
/**
|
|
1306
|
+
* The worktree registry (scripts/lib/worktrees.mjs is its one caller): reserve a row for a worktree about to be created,
|
|
1307
|
+
* atomically against the per-repo cap. BEGIN IMMEDIATE: two dispatches never both take the last slot. `cap` null: no cap.
|
|
1308
|
+
* {ok, live} | {ok:false, reason:'worktree-cap', live, cap}
|
|
1309
|
+
*/
|
|
1310
|
+
function reserveWorktree(m, wt, { cap = null } = {}) {
|
|
1311
|
+
return m.transaction((db) => {
|
|
1312
|
+
const repoRoot = repoKey(wt.repoRoot);
|
|
1313
|
+
const target = path.resolve(wt.path);
|
|
1314
|
+
const live = db.prepare('SELECT path FROM worktrees WHERE repo_root=? AND removed_at IS NULL').all(repoRoot).filter((r) => r.path !== target).length;
|
|
1315
|
+
if (cap != null && live >= cap) return { ok: false, reason: 'worktree-cap', live, cap };
|
|
1316
|
+
upsertRow(db, 'worktrees', { ...snake(wt), path: target, repo_root: repoRoot, created_at: m.now(), removed_at: null, remove_error: null }, ['path']);
|
|
1317
|
+
return { ok: true, live: live + 1 };
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
/** Live (not removed) registry rows, optionally of one repo. */
|
|
1321
|
+
const liveWorktrees = (m, { repoRoot = null } = {}) => m.db.prepare(`SELECT * FROM worktrees WHERE removed_at IS NULL ${repoRoot ? 'AND repo_root=?' : ''} ORDER BY created_at`)
|
|
1322
|
+
.all(...(repoRoot ? [repoKey(repoRoot)] : []));
|
|
1323
|
+
const worktreeRow = (m, wtPath) => m.db.prepare('SELECT * FROM worktrees WHERE path=?').get(path.resolve(wtPath)) ?? null;
|
|
1324
|
+
/** Every repo the registry ever held a worktree of. */
|
|
1325
|
+
const worktreeRepos = (m) => m.db.prepare('SELECT DISTINCT repo_root FROM worktrees ORDER BY repo_root').all().map((r) => r.repo_root);
|
|
1326
|
+
/** A reservation whose `git worktree add` failed: the row goes, nothing was created. */
|
|
1327
|
+
/** A GC collector's resume point (machine_meta gc_cursor:<name>): where its last bounded pass stopped, or null. */
|
|
1328
|
+
const gcCursor = (m, name) => m.db.prepare('SELECT value FROM machine_meta WHERE key=?').get(`gc_cursor:${name}`)?.value ?? null;
|
|
1329
|
+
const setGcCursor = (m, name, value) => (value == null
|
|
1330
|
+
? m.db.prepare('DELETE FROM machine_meta WHERE key=?').run(`gc_cursor:${name}`)
|
|
1331
|
+
: m.db.prepare('INSERT INTO machine_meta(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value').run(`gc_cursor:${name}`, String(value))).changes > 0;
|
|
1332
|
+
/** The worktree GC's stop mark (a removal changed the main checkout): {at, damage, path} or null. set(null) clears it. */
|
|
1333
|
+
const worktreeGcStop = (m) => parse(m.db.prepare("SELECT value FROM machine_meta WHERE key='worktree_gc_stopped'").get()?.value ?? null);
|
|
1334
|
+
const setWorktreeGcStop = (m, stop) => (stop
|
|
1335
|
+
? m.db.prepare("INSERT INTO machine_meta(key,value) VALUES('worktree_gc_stopped',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(JSON.stringify(stop))
|
|
1336
|
+
: m.db.prepare("DELETE FROM machine_meta WHERE key='worktree_gc_stopped'").run()).changes > 0;
|
|
1337
|
+
const dropWorktree = (m, wtPath) => m.db.prepare('DELETE FROM worktrees WHERE path=? AND removed_at IS NULL').run(path.resolve(wtPath)).changes > 0;
|
|
1257
1338
|
const upsertEnvServer = (m, server) => upsertRow(m.db, 'env_servers', snake(server), ['server_id']);
|
|
1258
1339
|
const envServer = (m, serverId) => m.db.prepare('SELECT * FROM env_servers WHERE server_id=?').get(serverId) ?? null;
|
|
1259
1340
|
const envServers = (m, { live = false } = {}) => m.db.prepare(`SELECT * FROM env_servers ${live ? "WHERE state IN ('starting','ready')" : ''} ORDER BY server_id`).all();
|
|
@@ -1443,13 +1524,13 @@ const API = {
|
|
|
1443
1524
|
upsertGuardJob, guardJob, releaseGuardJob, recordGuardRefusal, releaseHostLeases, release: releaseHostLeases, hostLeases, setBudget, reserveBudget, settleBudget, budgets,
|
|
1444
1525
|
startGcRun, finishGcRun, recordGcItem, addGcItem: recordGcItem, updateGcItem, gcItems, gcMark, addGcMarks: gcMark, gcRuns, markMachineBlobArchived, pruneSeatSnapshots,
|
|
1445
1526
|
upsertLane, setLaneState, laneOf, lanes, enqueueLand, claimLandGate, finishLandTicket, landQueue, recordLandRun, recordLandOutcome, landRuns, recordPush, pushes, flushOutbox,
|
|
1446
|
-
upsertWorktree, removedWorktree, upsertEnvServer, envServer, envServers, upsertUatSlot, uatSlots, releaseUatSlot, upsertConnector, connectorOf, upsertAsk,
|
|
1527
|
+
upsertWorktree, removedWorktree, reserveWorktree, liveWorktrees, worktreeRow, worktreeRepos, dropWorktree, worktreeGcStop, setWorktreeGcStop, gcCursor, setGcCursor, upsertEnvServer, envServer, envServers, upsertUatSlot, uatSlots, releaseUatSlot, upsertConnector, connectorOf, upsertAsk,
|
|
1447
1528
|
log, logs, pruneLogs, recordMetrics, latestMetrics, recordNotification, notificationSent, markNotificationSent, recordArchive, projectCatalog,
|
|
1448
1529
|
};
|
|
1449
1530
|
|
|
1450
1531
|
/** Every typed function at module level too: fn(handle, ...args) — blob-gc and callers holding a handle. */
|
|
1451
1532
|
export {
|
|
1452
|
-
putMachineBlob, jsonOrBlob, meta, checkpoint, registerLedger, resolveLedger, listLedgers, touchLedger, setLedgerState, upsertRepository, forEachLedger, attachFleet, supEvent, supEvents, newestSupEvent, upsertSupJob, setSupJobStatus, supJob, listSupJobs, acquireSupLeases, releaseSupLeases, supLeases, startSupAttempt, updateSupAttempt, latestSupAttempt, recordSupReport, supReports, consumeSupReport, openSupDecision, setSupDecision, markSupDecisionDelivered, listSupDecisions, openOwed, ackOwed, closeOwed, listOwed, upsertLearning, listLearning, recordOwnerRuling, upsertBridge, recordSupMessage, supMessages, markSupMessagesRead, setSupSignal, supSignal, clearSupSignal, recordMachineLlmUsage, startProcessRun, heartbeatProcessRun, endProcessRun, openProcessRuns, processRuns, leaderOf, acquireLeader, renewLeader, releaseLeader, leaderHistory, cursorOf, setCursor, cursors, enqueue, dueQueue, queueRows, dequeue, requeue, ensureSchedule, claimSchedule, finishSchedule, schedules, actionIntent, actionRunning, actionFinish, markStaleActionsUnknown, actionOf, actions, actionStep, controllerModes, setControllerMode, modeChanges, openSlaEpisode, markSlaViolated, markSlaReported, clearSla, openSla, recordViolation, clearViolation, setService, recordProbe, services, serviceEvents, upsertSeat, seatOf, seats, recordDelivery, recordSeatInput, startSeatTurn, endSeatTurn, seatTranscriptSnapshot, upsertTerminal, closeTerminal, openTerminals, acquireHostLock, renewHostLock, releaseHostLock, hostLock, hostLocks, claimResource, releaseClaim, sweptClaim, liveClaims, upsertAgentSession, inventorySnapshot, throttleState, setThrottle, throttleEvents, recordThrottleDecision, releaseThrottleDecision, recordHostSample, hostSamples, setProviderHealth, providerHealth, poolBackoff, setPoolBackoff, clearPoolBackoff, setQuota, quotas, upsertGuardJob, guardJob, releaseGuardJob, recordGuardRefusal, releaseHostLeases, hostLeases, setBudget, reserveBudget, settleBudget, budgets, startGcRun, finishGcRun, recordGcItem, updateGcItem, gcItems, gcMark, gcRuns, markMachineBlobArchived, pruneSeatSnapshots, upsertLane, setLaneState, laneOf, lanes, enqueueLand, claimLandGate, finishLandTicket, landQueue, recordLandRun, recordLandOutcome, landRuns, recordPush, pushes, flushOutbox, upsertWorktree, removedWorktree, upsertEnvServer, envServer, envServers, upsertUatSlot, uatSlots, releaseUatSlot, upsertConnector, connectorOf, upsertAsk, log, logs, pruneLogs, recordMetrics, latestMetrics, recordNotification, notificationSent, markNotificationSent, recordArchive, projectCatalog,
|
|
1533
|
+
putMachineBlob, jsonOrBlob, meta, checkpoint, registerLedger, resolveLedger, listLedgers, touchLedger, setLedgerState, upsertRepository, forEachLedger, attachFleet, supEvent, supEvents, newestSupEvent, upsertSupJob, setSupJobStatus, supJob, listSupJobs, acquireSupLeases, releaseSupLeases, supLeases, startSupAttempt, updateSupAttempt, latestSupAttempt, recordSupReport, supReports, consumeSupReport, openSupDecision, setSupDecision, markSupDecisionDelivered, listSupDecisions, openOwed, ackOwed, closeOwed, listOwed, upsertLearning, listLearning, recordOwnerRuling, upsertBridge, recordSupMessage, supMessages, markSupMessagesRead, setSupSignal, supSignal, clearSupSignal, recordMachineLlmUsage, startProcessRun, heartbeatProcessRun, endProcessRun, openProcessRuns, processRuns, leaderOf, acquireLeader, renewLeader, releaseLeader, leaderHistory, cursorOf, setCursor, cursors, enqueue, dueQueue, queueRows, dequeue, requeue, ensureSchedule, claimSchedule, finishSchedule, schedules, actionIntent, actionRunning, actionFinish, markStaleActionsUnknown, actionOf, actions, actionStep, controllerModes, setControllerMode, modeChanges, openSlaEpisode, markSlaViolated, markSlaReported, clearSla, openSla, recordViolation, clearViolation, setService, recordProbe, services, serviceEvents, upsertSeat, seatOf, seats, recordDelivery, recordSeatInput, startSeatTurn, endSeatTurn, seatTranscriptSnapshot, upsertTerminal, closeTerminal, openTerminals, acquireHostLock, renewHostLock, releaseHostLock, hostLock, hostLocks, claimResource, releaseClaim, sweptClaim, liveClaims, upsertAgentSession, inventorySnapshot, throttleState, setThrottle, throttleEvents, recordThrottleDecision, releaseThrottleDecision, recordHostSample, hostSamples, setProviderHealth, providerHealth, poolBackoff, setPoolBackoff, clearPoolBackoff, setQuota, quotas, upsertGuardJob, guardJob, releaseGuardJob, recordGuardRefusal, releaseHostLeases, hostLeases, setBudget, reserveBudget, settleBudget, budgets, startGcRun, finishGcRun, recordGcItem, updateGcItem, gcItems, gcMark, gcRuns, markMachineBlobArchived, pruneSeatSnapshots, upsertLane, setLaneState, laneOf, lanes, enqueueLand, claimLandGate, finishLandTicket, landQueue, recordLandRun, recordLandOutcome, landRuns, recordPush, pushes, flushOutbox, upsertWorktree, removedWorktree, reserveWorktree, liveWorktrees, worktreeRow, worktreeRepos, dropWorktree, worktreeGcStop, setWorktreeGcStop, gcCursor, setGcCursor, upsertEnvServer, envServer, envServers, upsertUatSlot, uatSlots, releaseUatSlot, upsertConnector, connectorOf, upsertAsk, log, logs, pruneLogs, recordMetrics, latestMetrics, recordNotification, notificationSent, markNotificationSent, recordArchive, projectCatalog,
|
|
1453
1534
|
};
|
|
1454
1535
|
export const addGcItem = recordGcItem;
|
|
1455
1536
|
export const addGcMarks = gcMark;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- 0002-worktrees-no-workflow-kind (starci/machine@1, user_version 1 -> 2). Applied by engine/machine-db.mjs migrateMachine on
|
|
2
|
+
-- the first writer open of an older machine.sqlite (after an integrity_check and a VACUUM INTO backup, with foreign_key_check
|
|
3
|
+
-- + quick_check before COMMIT); a fresh machine.sqlite runs 0001 then the forward files.
|
|
4
|
+
--
|
|
5
|
+
-- The per-workflow worktree is removed: a workflow owns no checkout of its own, an op works in its job worktree and a land
|
|
6
|
+
-- in its scratch. worktrees.kind no longer accepts 'workflow'. A row of that kind describes the removed mechanism and is
|
|
7
|
+
-- deleted (a directory it named, if any is left, is an unregistered worktree the worktree GC and the footprint scan report);
|
|
8
|
+
-- then the table's CHECK is rewritten in place (writable_schema: the views over worktrees stay as they are).
|
|
9
|
+
DELETE FROM worktrees WHERE kind='workflow';
|
|
10
|
+
PRAGMA writable_schema=ON;
|
|
11
|
+
UPDATE sqlite_master
|
|
12
|
+
SET sql=replace(sql, 'CHECK(kind IN (''op'',''workflow'',''land-scratch'',', 'CHECK(kind IN (''op'',''land-scratch'',')
|
|
13
|
+
WHERE type='table' AND name='worktrees';
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
-- 0005-ended-workflow-views (starci/runtime@1, user_version 4 -> 5). Applied by engine/ledger-db.mjs migrateLedger on the first
|
|
2
|
+
-- writer open of an older ledger (after a VACUUM INTO backup and an integrity_check, with foreign_key_check + quick_check
|
|
3
|
+
-- before COMMIT); a fresh ledger runs 0001 then the forward files.
|
|
4
|
+
--
|
|
5
|
+
-- The runtime views stop surfacing the live leftovers of an ENDED workflow (phase archived|finished): archive/finish close
|
|
6
|
+
-- the workflow's live Decision Items inside their own transaction before the phase flips, and events_refuse_archived refuses
|
|
7
|
+
-- every later write, so a leftover live DI (2026-09-30 nivo-backend di-876124d7) can never be resolved and only ever reads
|
|
8
|
+
-- 'bad' in the UI — a permanent attention item and blocker on work that no longer runs. Every leg carries the ENDED
|
|
9
|
+
-- predicate of scripts/reconciler/decisions.mjs listDecisions: LEFT JOIN workflows w; the row survives when its workflow is
|
|
10
|
+
-- missing or not ended (w.phase IS NULL OR w.phase NOT IN ('archived','finished')).
|
|
11
|
+
-- v_decision_rows keeps an ended workflow's resolved rows (decision history) and drops only its live-status leftovers;
|
|
12
|
+
-- v_blocking applies the join per UNION leg — an ended workflow owes no unit, decision, condition, incident,
|
|
13
|
+
-- worker-question, settle-tail, product-land or settle-overdue row;
|
|
14
|
+
-- v_open_work the same per leg (decision + job).
|
|
15
|
+
DROP VIEW IF EXISTS v_decision_rows;
|
|
16
|
+
DROP VIEW IF EXISTS v_blocking;
|
|
17
|
+
DROP VIEW IF EXISTS v_open_work;
|
|
18
|
+
CREATE VIEW v_decision_rows AS
|
|
19
|
+
SELECT d.*,
|
|
20
|
+
(d.due_at IS NOT NULL AND d.status IN ('open','claimed','escalated') AND d.due_at < CAST(unixepoch('subsec')*1000 AS INTEGER)) AS overdue,
|
|
21
|
+
CASE WHEN d.status IN ('open','claimed','escalated')
|
|
22
|
+
AND ((d.due_at IS NOT NULL AND d.due_at < CAST(unixepoch('subsec')*1000 AS INTEGER)) OR d.escalations>=2) THEN 'bad'
|
|
23
|
+
WHEN d.status='open' AND d.due_at IS NOT NULL
|
|
24
|
+
AND (d.due_at - CAST(unixepoch('subsec')*1000 AS INTEGER)) < 0.2*(d.due_at - d.opened_at) THEN 'warn'
|
|
25
|
+
ELSE COALESCE(m.ui,'unknown') END AS ui
|
|
26
|
+
FROM decision_items d LEFT JOIN ui_state_map m ON m.entity='decision' AND m.native=d.status
|
|
27
|
+
LEFT JOIN workflows w ON w.workflow_id=d.workflow_id
|
|
28
|
+
WHERE d.status NOT IN ('open','claimed','escalated') OR w.phase IS NULL OR w.phase NOT IN ('archived','finished');
|
|
29
|
+
|
|
30
|
+
CREATE VIEW v_blocking AS
|
|
31
|
+
SELECT e.workflow_id, 'unit' AS entity_type, e.to_unit AS entity_id, 'unit' AS blocker_type, e.from_unit AS blocker_id,
|
|
32
|
+
'UpstreamNotDone' AS reason_code, p.state AS detail, p.updated_at AS since, 'kernel' AS who
|
|
33
|
+
FROM unit_edges e JOIN work_units t ON t.workflow_id=e.workflow_id AND t.unit_id=e.to_unit
|
|
34
|
+
JOIN work_units p ON p.workflow_id=e.workflow_id AND p.unit_id=e.from_unit
|
|
35
|
+
LEFT JOIN workflows w ON w.workflow_id=e.workflow_id
|
|
36
|
+
WHERE t.state IN ('planned','queued') AND p.state NOT IN ('done','dropped')
|
|
37
|
+
AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
|
|
38
|
+
UNION ALL
|
|
39
|
+
SELECT d.workflow_id, COALESCE(d.entity_type,'workflow'), COALESCE(d.entity_id,d.workflow_id), 'decision', d.di_id,
|
|
40
|
+
'DecisionOpen:'||d.kind, d.summary, d.opened_at, d.decider
|
|
41
|
+
FROM decision_items d LEFT JOIN workflows w ON w.workflow_id=d.workflow_id
|
|
42
|
+
WHERE d.status IN ('open','claimed','escalated')
|
|
43
|
+
AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
|
|
44
|
+
UNION ALL
|
|
45
|
+
SELECT c.workflow_id, c.entity_type, c.entity_id, 'condition', c.type, c.reason, c.message, c.last_transition_at, COALESCE(c.owner,'controller')
|
|
46
|
+
FROM conditions c LEFT JOIN workflows w ON w.workflow_id=c.workflow_id
|
|
47
|
+
WHERE c.status<>'True'
|
|
48
|
+
AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
|
|
49
|
+
UNION ALL
|
|
50
|
+
SELECT i.workflow_id, 'job', COALESCE(i.job_id,i.workflow_id), 'incident', i.incident_id, 'IncidentOpen:'||i.kind,
|
|
51
|
+
COALESCE(i.detail,i.last_progress), i.created_at, i.owner
|
|
52
|
+
FROM incidents i LEFT JOIN workflows w ON w.workflow_id=i.workflow_id
|
|
53
|
+
WHERE i.status='open'
|
|
54
|
+
AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
|
|
55
|
+
UNION ALL
|
|
56
|
+
SELECT x.workflow_id, 'attempt', CAST(x.attempt_id AS TEXT), 'question', CAST(x.inbox_id AS TEXT), 'WorkerQuestionPending',
|
|
57
|
+
json_extract(x.payload_json,'$.question'), x.created_at, 'kernel'
|
|
58
|
+
FROM inbox x LEFT JOIN workflows w ON w.workflow_id=x.workflow_id
|
|
59
|
+
WHERE x.kind='worker-question' AND x.status IN ('pending','claimed')
|
|
60
|
+
AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
|
|
61
|
+
UNION ALL
|
|
62
|
+
SELECT s.workflow_id, 'attempt', CAST(s.attempt_id AS TEXT), 'settle-tail', CAST(s.attempt_id AS TEXT), 'SettleTailFailed', s.last_error,
|
|
63
|
+
COALESCE(s.started_at,s.queued_at), 'controller'
|
|
64
|
+
FROM settle_tails s LEFT JOIN workflows w ON w.workflow_id=s.workflow_id
|
|
65
|
+
WHERE s.state='failed'
|
|
66
|
+
AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
|
|
67
|
+
UNION ALL
|
|
68
|
+
SELECT l.workflow_id, 'workflow', l.workflow_id, 'product-land', CAST(l.land_id AS TEXT), 'ProductLand:'||l.result, l.reason,
|
|
69
|
+
l.started_at, 'kernel'
|
|
70
|
+
FROM product_lands l LEFT JOIN workflows w ON w.workflow_id=l.workflow_id
|
|
71
|
+
WHERE l.result IN ('failed','conflict','red')
|
|
72
|
+
AND NOT EXISTS(SELECT 1 FROM product_lands k WHERE k.workflow_id=l.workflow_id AND k.land_id>l.land_id AND k.result='landed')
|
|
73
|
+
AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
|
|
74
|
+
UNION ALL
|
|
75
|
+
-- H1: a filed report nobody settled within its SLA (the same rows as v_settle_overdue) blocks its job.
|
|
76
|
+
SELECT a.workflow_id, 'attempt', CAST(a.attempt_id AS TEXT), 'settle', CAST(a.attempt_id AS TEXT),
|
|
77
|
+
'SettleOverdue:'||COALESCE(a.report_outcome,'?'), a.job_id, a.reported_at,
|
|
78
|
+
CASE WHEN a.report_outcome='done' THEN 'settler' ELSE 'kernel' END
|
|
79
|
+
FROM op_attempts a LEFT JOIN workflows w ON w.workflow_id=a.workflow_id
|
|
80
|
+
WHERE a.reported_at IS NOT NULL AND a.settled_at IS NULL AND a.end_state IS NULL
|
|
81
|
+
AND (CAST(unixepoch('subsec')*1000 AS INTEGER) - a.reported_at) > CASE WHEN a.report_outcome='done' THEN 180000 ELSE 900000 END
|
|
82
|
+
AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'));
|
|
83
|
+
|
|
84
|
+
CREATE VIEW v_open_work AS
|
|
85
|
+
SELECT 'decision' AS kind, d.workflow_id, d.di_id AS ref, d.summary AS what, d.opened_at AS since, d.due_at
|
|
86
|
+
FROM decision_items d LEFT JOIN workflows w ON w.workflow_id=d.workflow_id
|
|
87
|
+
WHERE d.status IN ('open','claimed','escalated')
|
|
88
|
+
AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
|
|
89
|
+
UNION ALL
|
|
90
|
+
SELECT 'job-'||j.status, j.workflow_id, j.job_id, j.op_id, j.updated_at, j.deadline
|
|
91
|
+
FROM jobs j LEFT JOIN workflows w ON w.workflow_id=j.workflow_id
|
|
92
|
+
WHERE j.status IN ('leased','running','answering','effect_unknown')
|
|
93
|
+
AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'));
|
|
@@ -5,38 +5,45 @@ purpose: >-
|
|
|
5
5
|
then the repositories' own upgrade lane (each sets the exact version and reinstalls; nothing else installs a @starci package). Read on 2026-09-29 from the packages under
|
|
6
6
|
.claude/packages and from the installed node_modules of nivo-backend, nivo-fe, starci-next, starci-next-fe,
|
|
7
7
|
mia-mia-backend and miamia-fe; where they disagreed the newest version installed in more than one repository
|
|
8
|
-
won, and the runtime's own version won for @starci packages.
|
|
8
|
+
won, and the runtime's own version won for @starci packages. An app (hfs 4) has ONE package.json for both sides, so a
|
|
9
|
+
dependency both sides declared at different versions takes one: its pin where it has one, otherwise the higher of the
|
|
10
|
+
two, pinned here with a why (2026-10-01, from nivo-backend/nivo-fe, starci-next/starci-next-fe and
|
|
11
|
+
mia-mia-backend/miamia-fe: every such conflict but graphql was already pinned). The runtime's example apps merged the
|
|
12
|
+
same way on 2026-10-01 (examples/todo-app from todo-app-backend/todo-app-frontend, examples/ecommerce-app from
|
|
13
|
+
ecommerce-app-be/ecommerce-app-fe): no dependency outside this file was declared at two versions across their sides, so
|
|
14
|
+
every choice is a pin (each side's older @starci pins and graphql moved to the version here). `side` names the side whose code uses
|
|
15
|
+
the dependency; the app root package.json carries the pins of both.
|
|
9
16
|
pins:
|
|
10
17
|
# --- @starci packages. Every @starci package is a PUBLISHED npm package (install: registry): a product repository
|
|
11
18
|
# installs the exact pinned version from the npm registry in the root and every workspace package.json, never `file:`.
|
|
12
19
|
'@starci/grammar':
|
|
13
|
-
version: 0.
|
|
20
|
+
version: 0.8.0
|
|
14
21
|
group: starci
|
|
15
22
|
install: registry
|
|
16
23
|
side: fe
|
|
17
24
|
source: packages/grammar/package.json
|
|
18
|
-
why: nivo-fe pins 0.4.11 and 0.5.0 in one workspace, starci-next-fe 0.5.1, miamia-fe 0.5.0; the runtime source is 0.7.2
|
|
25
|
+
why: nivo-fe pins 0.4.11 and 0.5.0 in one workspace, starci-next-fe 0.5.1, miamia-fe 0.5.0; the runtime source is 0.8.0 (the brand layer sets `--font-sans` and `--font-mono`; the grammar reads them; 0.7.2 added the Input tel kind and IconButton disclosure props).
|
|
19
26
|
'@starci/eslint-canon-be':
|
|
20
|
-
version:
|
|
27
|
+
version: 3.0.0
|
|
21
28
|
group: starci
|
|
22
29
|
install: registry
|
|
23
30
|
side: be
|
|
24
31
|
source: packages/eslint/be/package.json
|
|
25
|
-
why: '2.0.0 (C0 release): starciBeConfig({ hfs: loadHfs(import.meta.url) }) typed factory and the BE-CONVENTION laws
|
|
32
|
+
why: '3.0.0: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side; the project graph is built per side. 2.0.0 (C0 release): starciBeConfig({ hfs: loadHfs(import.meta.url) }) typed factory and the BE-CONVENTION laws.'
|
|
26
33
|
'@starci/eslint-canon-fe':
|
|
27
|
-
version:
|
|
34
|
+
version: 8.0.0
|
|
28
35
|
group: starci
|
|
29
36
|
install: registry
|
|
30
37
|
side: fe
|
|
31
38
|
source: packages/eslint/fe/package.json
|
|
32
|
-
why: '7.0.0: the front end has no tests (
|
|
39
|
+
why: '8.0.0: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side; the project graph is built per side. 7.0.0: the front end has no tests (FE_NO_TESTS R97 in hfs); no-vietnamese-in-source (R91).'
|
|
33
40
|
'@starci/stylelint-canon':
|
|
34
|
-
version: 2.0.
|
|
41
|
+
version: 2.0.1
|
|
35
42
|
group: starci
|
|
36
43
|
install: registry
|
|
37
44
|
side: fe
|
|
38
45
|
source: packages/stylelint/package.json
|
|
39
|
-
why: '2.0.0 adds status-contrast (HeroUI soft pairs measured per theme), fixes brand-layer-shape on the shared :root,.light,.dark block and the info soft pair, and derives appTokens with loadAppTokens(import.meta.url) for the managed one-line stylelint.config.mjs.'
|
|
46
|
+
why: '2.0.1 accepts `--font-sans` and `--font-mono` in the brand layer (the vocabulary follows the grammar 0.8.0). 2.0.0 adds status-contrast (HeroUI soft pairs measured per theme), fixes brand-layer-shape on the shared :root,.light,.dark block and the info soft pair, and derives appTokens with loadAppTokens(import.meta.url) for the managed one-line stylelint.config.mjs.'
|
|
40
47
|
'@starci/tsconfig':
|
|
41
48
|
version: 2.0.0
|
|
42
49
|
group: starci
|
|
@@ -50,19 +57,26 @@ pins:
|
|
|
50
57
|
side: both
|
|
51
58
|
source: packages/prettier-config/package.json
|
|
52
59
|
'@starci/jest-preset':
|
|
53
|
-
version: 2.
|
|
60
|
+
version: 2.2.0
|
|
54
61
|
group: starci
|
|
55
62
|
install: registry
|
|
56
63
|
side: be
|
|
57
64
|
source: packages/jest-preset/package.json
|
|
58
|
-
why: '2.1.0: four projects on the test world, the unit kit (mockEntityManager, fakeTransaction, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
|
|
65
|
+
why: '2.2.0: the integration, e2e and contract projects run every spec file in a worker process of its own (world-runner.cjs), so a process-global framework registry (@nestjs/graphql type metadata) never leaks between e2e files; 2.1.0: four projects on the test world, the unit kit (mockEntityManager, fakeTransaction, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
|
|
66
|
+
'@starci/test-world':
|
|
67
|
+
version: 1.0.0
|
|
68
|
+
group: starci
|
|
69
|
+
install: registry
|
|
70
|
+
side: be
|
|
71
|
+
source: packages/test-world/package.json
|
|
72
|
+
why: 'the shared e2e library of every back end (R47, R48): the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs, the outage lock and the starci-test-stack bin behind the managed test:stack script; a devDependency of every back end.'
|
|
59
73
|
'@starci/hfs':
|
|
60
|
-
version:
|
|
74
|
+
version: 4.0.0
|
|
61
75
|
group: starci
|
|
62
76
|
install: registry
|
|
63
77
|
side: both
|
|
64
78
|
source: packages/hfs/package.json
|
|
65
|
-
why: '
|
|
79
|
+
why: '4.0.0: the app monorepo standard: one app repository with the root package.json, lockfile and hfs.json of kind app, and the be/ and fe/ sides; `hfs scaffold app <name>` makes it, `hfs lint` at the root lints be/** with the BE canon and fe/** with the FE canon; the standalone be/fe repository kinds and `hfs init` are deleted.'
|
|
66
80
|
# --- tooling
|
|
67
81
|
typescript:
|
|
68
82
|
version: 5.9.3
|
|
@@ -152,6 +166,11 @@ pins:
|
|
|
152
166
|
group: framework
|
|
153
167
|
side: fe
|
|
154
168
|
why: every HFS front end uses next-intl with the [locale] segment (hfs sync --init writes it); nivo-fe resolves 4.13.6.
|
|
169
|
+
graphql:
|
|
170
|
+
version: 16.14.2
|
|
171
|
+
group: framework
|
|
172
|
+
side: both
|
|
173
|
+
why: 'one app package.json for both sides (hfs 4): nivo-backend declares ^16.14.1 and nivo-fe ^16.14.2, the higher wins; all four product installs resolve 16.14.2. The example apps: todo-app-backend declared ^16.14.2 (devDependencies) and ecommerce-app-be ^16.14.2 (dependencies and devDependencies); each app root now has one dependencies entry at the pin.'
|
|
155
174
|
supertest:
|
|
156
175
|
version: 7.3.0
|
|
157
176
|
group: tooling
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
schema: starci/hfs-peer-integrations@1
|
|
2
|
+
purpose: >-
|
|
3
|
+
The runtime peers a driver integration needs and npm does not install for it: "if the app depends on every package of
|
|
4
|
+
`when` (at the named major, when one is named), it must declare `requires` in its dependencies". A missing peer is
|
|
5
|
+
invisible to the type checker and the unit specs and only fails when the api boots (todo-app and ecommerce-app,
|
|
6
|
+
2026-10-01: neither old backend declared @as-integrations/express5, so the Apollo GraphQL api could not boot under
|
|
7
|
+
Express 5 and nothing noticed before e2e). R111 HFS_PEER_INTEGRATION_MISSING judges the app root package.json, the one
|
|
8
|
+
manifest of both sides, with this catalog; an entry is added the day a boot failure of the same kind is found.
|
|
9
|
+
pairs:
|
|
10
|
+
- id: nestjs-apollo-express5
|
|
11
|
+
when:
|
|
12
|
+
- {package: "@nestjs/apollo"}
|
|
13
|
+
- {package: "@nestjs/platform-express", major: 11}
|
|
14
|
+
requires: "@as-integrations/express5"
|
|
15
|
+
why: >-
|
|
16
|
+
@nestjs/platform-express 11 runs Express 5, and Apollo Server 5 serves an Express 5 app only through
|
|
17
|
+
@as-integrations/express5, which @nestjs/apollo loads at boot and does not depend on; without it the GraphQL api
|
|
18
|
+
does not start.
|