@cleocode/core 2026.10.18 → 2026.10.19
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/dist/build-provenance.json +2 -2
- package/dist/check/pr-gate.js +1 -1
- package/dist/check/pr-gate.js.map +1 -1
- package/dist/cloud/nexus-vault.d.ts +4 -3
- package/dist/cloud/nexus-vault.d.ts.map +1 -1
- package/dist/cloud/nexus-vault.js +62 -22
- package/dist/cloud/nexus-vault.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -1
- package/dist/docs/conduit-history-backfill.d.ts +210 -0
- package/dist/docs/conduit-history-backfill.d.ts.map +1 -0
- package/dist/docs/conduit-history-backfill.js +451 -0
- package/dist/docs/conduit-history-backfill.js.map +1 -0
- package/dist/docs/docs-ops.d.ts +33 -0
- package/dist/docs/docs-ops.d.ts.map +1 -1
- package/dist/docs/docs-ops.js +72 -0
- package/dist/docs/docs-ops.js.map +1 -1
- package/dist/docs/docs-update.d.ts.map +1 -1
- package/dist/docs/docs-update.js +71 -0
- package/dist/docs/docs-update.js.map +1 -1
- package/dist/docs/doctor.d.ts +18 -4
- package/dist/docs/doctor.d.ts.map +1 -1
- package/dist/docs/doctor.js +124 -3
- package/dist/docs/doctor.js.map +1 -1
- package/dist/docs/section-merge.d.ts +70 -9
- package/dist/docs/section-merge.d.ts.map +1 -1
- package/dist/docs/section-merge.js +167 -10
- package/dist/docs/section-merge.js.map +1 -1
- package/dist/docs/section-write-path.d.ts.map +1 -1
- package/dist/docs/section-write-path.js +23 -8
- package/dist/docs/section-write-path.js.map +1 -1
- package/dist/gateway-client/generated/types.gen.d.ts +4 -0
- package/dist/gateway-client/generated/types.gen.d.ts.map +1 -1
- package/dist/go/driver.d.ts +29 -145
- package/dist/go/driver.d.ts.map +1 -1
- package/dist/go/driver.js +75 -168
- package/dist/go/driver.js.map +1 -1
- package/dist/go/index.d.ts +1 -1
- package/dist/go/index.d.ts.map +1 -1
- package/dist/go/index.js +1 -1
- package/dist/go/index.js.map +1 -1
- package/dist/goal/close.d.ts +1 -1
- package/dist/goal/close.js +2 -2
- package/dist/goal/close.js.map +1 -1
- package/dist/goal/draft.d.ts +10 -3
- package/dist/goal/draft.d.ts.map +1 -1
- package/dist/goal/draft.js +14 -9
- package/dist/goal/draft.js.map +1 -1
- package/dist/goal/index.d.ts +9 -12
- package/dist/goal/index.d.ts.map +1 -1
- package/dist/goal/index.js +9 -12
- package/dist/goal/index.js.map +1 -1
- package/dist/goal/runtime.d.ts +126 -0
- package/dist/goal/runtime.d.ts.map +1 -0
- package/dist/goal/runtime.js +289 -0
- package/dist/goal/runtime.js.map +1 -0
- package/dist/goal/status.d.ts +39 -0
- package/dist/goal/status.d.ts.map +1 -0
- package/dist/goal/status.js +101 -0
- package/dist/goal/status.js.map +1 -0
- package/dist/hooks/project-runner.d.ts +8 -3
- package/dist/hooks/project-runner.d.ts.map +1 -1
- package/dist/hooks/project-runner.js +104 -14
- package/dist/hooks/project-runner.js.map +1 -1
- package/dist/hooks/project-state.js +1 -1
- package/dist/hooks/project-state.js.map +1 -1
- package/dist/internal.d.ts +7 -3
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +5 -3
- package/dist/internal.js.map +1 -1
- package/dist/lifecycle/engine-ops.d.ts.map +1 -1
- package/dist/lifecycle/engine-ops.js +9 -6
- package/dist/lifecycle/engine-ops.js.map +1 -1
- package/dist/memory/retrieval/observe.d.ts.map +1 -1
- package/dist/memory/retrieval/observe.js +4 -8
- package/dist/memory/retrieval/observe.js.map +1 -1
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +4 -1
- package/dist/paths.js.map +1 -1
- package/dist/playbooks/cantbook-profile.d.ts +3 -3
- package/dist/playbooks/cantbook-profile.js +3 -3
- package/dist/playbooks/pi-runner-wiring.d.ts +3 -3
- package/dist/playbooks/pi-runner-wiring.js +3 -3
- package/dist/release/ci-evidence.d.ts +28 -0
- package/dist/release/ci-evidence.d.ts.map +1 -1
- package/dist/release/ci-evidence.js +76 -20
- package/dist/release/ci-evidence.js.map +1 -1
- package/dist/release/open.d.ts +11 -0
- package/dist/release/open.d.ts.map +1 -1
- package/dist/release/open.js +49 -10
- package/dist/release/open.js.map +1 -1
- package/dist/release/preflight-skip.d.ts +50 -0
- package/dist/release/preflight-skip.d.ts.map +1 -1
- package/dist/release/preflight-skip.js +151 -34
- package/dist/release/preflight-skip.js.map +1 -1
- package/dist/release/reconcile.d.ts.map +1 -1
- package/dist/release/reconcile.js +6 -3
- package/dist/release/reconcile.js.map +1 -1
- package/dist/scaffold/ensure-config.d.ts +1 -1
- package/dist/scaffold/ensure-config.d.ts.map +1 -1
- package/dist/scaffold/ensure-config.js +2 -0
- package/dist/scaffold/ensure-config.js.map +1 -1
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/index.js.map +1 -1
- package/dist/security/override-cap-setting.d.ts +180 -0
- package/dist/security/override-cap-setting.d.ts.map +1 -0
- package/dist/security/override-cap-setting.js +657 -0
- package/dist/security/override-cap-setting.js.map +1 -0
- package/dist/security/override-cap.d.ts +20 -29
- package/dist/security/override-cap.d.ts.map +1 -1
- package/dist/security/override-cap.js +47 -71
- package/dist/security/override-cap.js.map +1 -1
- package/dist/security/owner-override-auth.d.ts +58 -0
- package/dist/security/owner-override-auth.d.ts.map +1 -1
- package/dist/security/owner-override-auth.js +82 -1
- package/dist/security/owner-override-auth.js.map +1 -1
- package/dist/security/owner-override-authority.d.ts +67 -0
- package/dist/security/owner-override-authority.d.ts.map +1 -0
- package/dist/security/owner-override-authority.js +100 -0
- package/dist/security/owner-override-authority.js.map +1 -0
- package/dist/sessions/handoff.d.ts.map +1 -1
- package/dist/sessions/handoff.js +4 -5
- package/dist/sessions/handoff.js.map +1 -1
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/index.js +22 -25
- package/dist/sessions/index.js.map +1 -1
- package/dist/store/conduit-doc-history.d.ts +39 -7
- package/dist/store/conduit-doc-history.d.ts.map +1 -1
- package/dist/store/conduit-doc-history.js +62 -12
- package/dist/store/conduit-doc-history.js.map +1 -1
- package/dist/store/data-safety-central.d.ts.map +1 -1
- package/dist/store/data-safety-central.js +3 -2
- package/dist/store/data-safety-central.js.map +1 -1
- package/dist/store/json.d.ts.map +1 -1
- package/dist/store/json.js +7 -7
- package/dist/store/json.js.map +1 -1
- package/dist/store/portable-bundle-scan.d.ts +23 -1
- package/dist/store/portable-bundle-scan.d.ts.map +1 -1
- package/dist/store/portable-bundle-scan.js +85 -3
- package/dist/store/portable-bundle-scan.js.map +1 -1
- package/dist/store/portable-bundle.d.ts.map +1 -1
- package/dist/store/portable-bundle.js +6 -1
- package/dist/store/portable-bundle.js.map +1 -1
- package/dist/store/row-identity-registry.d.ts +5 -3
- package/dist/store/row-identity-registry.d.ts.map +1 -1
- package/dist/store/row-identity-registry.js +54 -38
- package/dist/store/row-identity-registry.js.map +1 -1
- package/dist/store/schema/cleo-project/conduit.d.ts.map +1 -1
- package/dist/store/schema/cleo-project/conduit.js +4 -3
- package/dist/store/schema/cleo-project/conduit.js.map +1 -1
- package/dist/store/schema/cleo-project/runtime.d.ts +32 -0
- package/dist/store/schema/cleo-project/runtime.d.ts.map +1 -1
- package/dist/store/schema/cleo-project/runtime.js +7 -3
- package/dist/store/schema/cleo-project/runtime.js.map +1 -1
- package/dist/store/session-store.d.ts +45 -0
- package/dist/store/session-store.d.ts.map +1 -1
- package/dist/store/session-store.js +96 -1
- package/dist/store/session-store.js.map +1 -1
- package/dist/store/sqlite.d.ts +2 -0
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +12 -0
- package/dist/store/sqlite.js.map +1 -1
- package/dist/store/sync/merge/rules.d.ts.map +1 -1
- package/dist/store/sync/merge/rules.js +0 -16
- package/dist/store/sync/merge/rules.js.map +1 -1
- package/dist/store/sync/sealer.d.ts.map +1 -1
- package/dist/store/sync/sealer.js +27 -1
- package/dist/store/sync/sealer.js.map +1 -1
- package/dist/store/sync/sequencing.d.ts +3 -1
- package/dist/store/sync/sequencing.d.ts.map +1 -1
- package/dist/store/sync/sequencing.js +5 -3
- package/dist/store/sync/sequencing.js.map +1 -1
- package/dist/store/sync/timestamp-canon.d.ts.map +1 -1
- package/dist/store/sync/timestamp-canon.js +4 -0
- package/dist/store/sync/timestamp-canon.js.map +1 -1
- package/dist/store/table-classification.d.ts.map +1 -1
- package/dist/store/table-classification.js +12 -8
- package/dist/store/table-classification.js.map +1 -1
- package/dist/store/twin-collapse.d.ts +21 -0
- package/dist/store/twin-collapse.d.ts.map +1 -1
- package/dist/store/twin-collapse.js +235 -18
- package/dist/store/twin-collapse.js.map +1 -1
- package/dist/system/health.d.ts +17 -0
- package/dist/system/health.d.ts.map +1 -1
- package/dist/system/health.js +96 -0
- package/dist/system/health.js.map +1 -1
- package/dist/tasks/affected-packages.d.ts +67 -2
- package/dist/tasks/affected-packages.d.ts.map +1 -1
- package/dist/tasks/affected-packages.js +149 -21
- package/dist/tasks/affected-packages.js.map +1 -1
- package/dist/tasks/affected-scope.d.ts.map +1 -1
- package/dist/tasks/affected-scope.js +91 -5
- package/dist/tasks/affected-scope.js.map +1 -1
- package/dist/tasks/changeset-plan.d.ts +33 -0
- package/dist/tasks/changeset-plan.d.ts.map +1 -0
- package/dist/tasks/changeset-plan.js +152 -0
- package/dist/tasks/changeset-plan.js.map +1 -0
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/complete.js +9 -1
- package/dist/tasks/complete.js.map +1 -1
- package/dist/tasks/done-plan.d.ts.map +1 -1
- package/dist/tasks/done-plan.js +28 -14
- package/dist/tasks/done-plan.js.map +1 -1
- package/dist/tasks/done-record.d.ts.map +1 -1
- package/dist/tasks/done-record.js +5 -1
- package/dist/tasks/done-record.js.map +1 -1
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +149 -18
- package/dist/tasks/evidence.js.map +1 -1
- package/dist/tasks/gate-runner.d.ts.map +1 -1
- package/dist/tasks/gate-runner.js +76 -4
- package/dist/tasks/gate-runner.js.map +1 -1
- package/dist/tasks/qa-run-binding.d.ts +18 -0
- package/dist/tasks/qa-run-binding.d.ts.map +1 -1
- package/dist/tasks/qa-run-binding.js +41 -1
- package/dist/tasks/qa-run-binding.js.map +1 -1
- package/dist/tasks/test-run-binding.d.ts +62 -0
- package/dist/tasks/test-run-binding.d.ts.map +1 -1
- package/dist/tasks/test-run-binding.js +126 -21
- package/dist/tasks/test-run-binding.js.map +1 -1
- package/dist/tasks/tool-resolver.d.ts +7 -0
- package/dist/tasks/tool-resolver.d.ts.map +1 -1
- package/dist/tasks/tool-resolver.js +3 -1
- package/dist/tasks/tool-resolver.js.map +1 -1
- package/dist/validation/engine-ops.d.ts.map +1 -1
- package/dist/validation/engine-ops.js +24 -29
- package/dist/validation/engine-ops.js.map +1 -1
- package/migrations/drizzle-cleo-project/20261016120000_t12535-playbook-row-uids/migration.sql +24 -0
- package/package.json +13 -13
- package/schemas/project-context.schema.json +5 -0
- package/templates/CLEO-INJECTION.md +1 -1
- package/templates/CLEO-REFERENCE.md +42 -1
- package/templates/cleo-gitignore +3 -0
- package/templates/git-hooks/legacy-hashes.json +1 -0
- package/templates/git-hooks/pre-push +35 -7
- package/templates/workflows/release-prepare.yml.tmpl +82 -4
- package/dist/goal/advance-with-persist.d.ts +0 -80
- package/dist/goal/advance-with-persist.d.ts.map +0 -1
- package/dist/goal/advance-with-persist.js +0 -105
- package/dist/goal/advance-with-persist.js.map +0 -1
- package/dist/goal/arm.d.ts +0 -61
- package/dist/goal/arm.d.ts.map +0 -1
- package/dist/goal/arm.js +0 -65
- package/dist/goal/arm.js.map +0 -1
- package/dist/goal/continuation.d.ts +0 -66
- package/dist/goal/continuation.d.ts.map +0 -1
- package/dist/goal/continuation.js +0 -111
- package/dist/goal/continuation.js.map +0 -1
- package/dist/goal/judge.d.ts +0 -113
- package/dist/goal/judge.d.ts.map +0 -1
- package/dist/goal/judge.js +0 -193
- package/dist/goal/judge.js.map +0 -1
- package/dist/goal/loop.d.ts +0 -75
- package/dist/goal/loop.d.ts.map +0 -1
- package/dist/goal/loop.js +0 -132
- package/dist/goal/loop.js.map +0 -1
|
@@ -147,6 +147,13 @@
|
|
|
147
147
|
* retried at the next open; and a long backlog drains in bounded transactions.
|
|
148
148
|
* See `TOKEN_USAGE`.
|
|
149
149
|
*
|
|
150
|
+
* `playbook_runs` + `playbook_approvals` → `tasks_playbook_runs` +
|
|
151
|
+
* `tasks_playbook_approvals` (T12535) is a lossless CARRY then a freeze: this
|
|
152
|
+
* build reads and writes only the twins (which sync); every bare row the twins
|
|
153
|
+
* lack is copied (at the initial collapse a differing bare row wins and the
|
|
154
|
+
* twin's value is archived), nothing is deleted, and the bare tables get the
|
|
155
|
+
* same freeze triggers as the docs pair. See `PLAYBOOKS`.
|
|
156
|
+
*
|
|
150
157
|
* ## Contract
|
|
151
158
|
*
|
|
152
159
|
* 1. **Snapshot first** (initial collapse only, only when the merge changes
|
|
@@ -832,10 +839,14 @@ const STICKY_TAGS = {
|
|
|
832
839
|
deleted: counts.deleted + synced.deleted,
|
|
833
840
|
};
|
|
834
841
|
},
|
|
842
|
+
// @sync-invariant none:local-only fills this connection's TEMP read shadow; main is never written
|
|
835
843
|
shadow(db, plan) {
|
|
836
|
-
|
|
844
|
+
// Every column of the twin (its uid too), so a reader of the shadow sees
|
|
845
|
+
// the twin's shape (T12535: the table syncs).
|
|
846
|
+
db.exec('CREATE TEMP TABLE IF NOT EXISTS brain_sticky_tags AS SELECT * FROM main.brain_sticky_tags WHERE 0');
|
|
847
|
+
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS temp.brain_sticky_tags_shadow_key ON brain_sticky_tags (sticky_id, tag)');
|
|
837
848
|
db.exec('DELETE FROM temp.brain_sticky_tags');
|
|
838
|
-
db.exec('INSERT INTO temp.brain_sticky_tags
|
|
849
|
+
db.exec('INSERT INTO temp.brain_sticky_tags SELECT * FROM main.brain_sticky_tags');
|
|
839
850
|
applyStickyRows(db, 'temp', plan);
|
|
840
851
|
syncStickyJunction(db, 'temp');
|
|
841
852
|
},
|
|
@@ -965,7 +976,11 @@ export const DOCS_FROZEN_BY_KEY = 'twin_collapse_frozen_by:attachments';
|
|
|
965
976
|
* @task T12535
|
|
966
977
|
*/
|
|
967
978
|
export function docsFrozenVersion(db) {
|
|
968
|
-
|
|
979
|
+
return frozenVersion(db, DOCS_FROZEN_BY_KEY);
|
|
980
|
+
}
|
|
981
|
+
/** The version a freeze recorded under `byKey` names ({@link docsFrozenVersion}). */
|
|
982
|
+
function frozenVersion(db, byKey) {
|
|
983
|
+
const recorded = readKv(db, 'tasks_schema_meta', byKey);
|
|
969
984
|
if (recorded !== undefined)
|
|
970
985
|
return recorded.replace(/[^0-9A-Za-z.+-]/g, '');
|
|
971
986
|
return releaseBuildVersion();
|
|
@@ -1006,8 +1021,9 @@ export const DOCS_FROZEN_FORBIDDEN = [
|
|
|
1006
1021
|
'not a database',
|
|
1007
1022
|
];
|
|
1008
1023
|
/**
|
|
1009
|
-
* BEFORE INSERT/UPDATE/DELETE triggers on
|
|
1010
|
-
*
|
|
1024
|
+
* BEFORE INSERT/UPDATE/DELETE triggers on a pair's frozen bare tables (the
|
|
1025
|
+
* docs tables, the playbook tables) that abort every write with the pair's
|
|
1026
|
+
* message, e.g. {@link docsFrozenMessage} (option (a), owner decision
|
|
1011
1027
|
* D11160). SQLite enforces them whatever build writes, so the 2026.9.20 build
|
|
1012
1028
|
* can no longer write rows this build treats as frozen: no duplicate content
|
|
1013
1029
|
* or slug, no split ref counts, no stale-read deletes, no ADR number reuse.
|
|
@@ -1018,25 +1034,34 @@ export const DOCS_FROZEN_FORBIDDEN = [
|
|
|
1018
1034
|
* the triggers go with a renamed or dropped table and are re-installed at the
|
|
1019
1035
|
* next open, see {@link repairGuards}).
|
|
1020
1036
|
*/
|
|
1021
|
-
function
|
|
1022
|
-
if (readKv(db, 'tasks_schema_meta',
|
|
1037
|
+
function ensureFreeze(db, spec) {
|
|
1038
|
+
if (readKv(db, 'tasks_schema_meta', spec.byKey) === undefined) {
|
|
1023
1039
|
const version = releaseBuildVersion();
|
|
1024
1040
|
if (version !== null)
|
|
1025
|
-
writeKv(db, 'tasks_schema_meta',
|
|
1041
|
+
writeKv(db, 'tasks_schema_meta', spec.byKey, version);
|
|
1026
1042
|
}
|
|
1027
|
-
for (const table of
|
|
1043
|
+
for (const table of spec.tables) {
|
|
1028
1044
|
for (const op of SHADOW_WRITE_OPS) {
|
|
1029
1045
|
const name = freezeTrigger(table, op);
|
|
1030
1046
|
// A trigger with another text (an older message or version) is replaced.
|
|
1031
|
-
if (freezeSql(db, name) !== undefined && !freezeUpToDate(db, table, op))
|
|
1047
|
+
if (freezeSql(db, name) !== undefined && !freezeUpToDate(db, spec, table, op))
|
|
1032
1048
|
db.exec(`DROP TRIGGER main.${name}`);
|
|
1033
1049
|
db.exec(`CREATE TRIGGER IF NOT EXISTS main.${name} BEFORE ${op} ON ${table} ` +
|
|
1034
|
-
`BEGIN ${freezeBody(db)} END`);
|
|
1050
|
+
`BEGIN ${freezeBody(db, spec)} END`);
|
|
1035
1051
|
}
|
|
1036
1052
|
}
|
|
1037
1053
|
}
|
|
1054
|
+
const DOCS_FREEZE = {
|
|
1055
|
+
tables: DOCS_BARE,
|
|
1056
|
+
byKey: DOCS_FROZEN_BY_KEY,
|
|
1057
|
+
message: docsFrozenMessage,
|
|
1058
|
+
};
|
|
1038
1059
|
/** The freeze trigger's body (it carries the message). */
|
|
1039
|
-
const freezeBody = (db) => `SELECT RAISE(ABORT, '${
|
|
1060
|
+
const freezeBody = (db, spec) => `SELECT RAISE(ABORT, '${spec.message(frozenVersion(db, spec.byKey)).replace(/'/g, "''")}');`;
|
|
1061
|
+
/** Whether every freeze trigger of `spec` exists with its current text. */
|
|
1062
|
+
function freezeIntact(db, spec) {
|
|
1063
|
+
return spec.tables.every((t) => SHADOW_WRITE_OPS.every((op) => freezeUpToDate(db, spec, t, op)));
|
|
1064
|
+
}
|
|
1040
1065
|
/** The stored SQL of a trigger, or `undefined` when it does not exist. */
|
|
1041
1066
|
function freezeSql(db, name) {
|
|
1042
1067
|
return db
|
|
@@ -1044,9 +1069,9 @@ function freezeSql(db, name) {
|
|
|
1044
1069
|
.get(name)?.sql;
|
|
1045
1070
|
}
|
|
1046
1071
|
/** Whether a freeze trigger exists with the current table, event and message. */
|
|
1047
|
-
function freezeUpToDate(db, table, op) {
|
|
1072
|
+
function freezeUpToDate(db, spec, table, op) {
|
|
1048
1073
|
const sql = freezeSql(db, freezeTrigger(table, op));
|
|
1049
|
-
return sql?.includes(`BEFORE ${op} ON ${table} `) === true && sql.includes(freezeBody(db));
|
|
1074
|
+
return sql?.includes(`BEFORE ${op} ON ${table} `) === true && sql.includes(freezeBody(db, spec));
|
|
1050
1075
|
}
|
|
1051
1076
|
/** Whether every freeze and change trigger is in place. */
|
|
1052
1077
|
function docsGuardsIntact(db) {
|
|
@@ -1056,11 +1081,11 @@ function docsGuardsIntact(db) {
|
|
|
1056
1081
|
.get(...names).n;
|
|
1057
1082
|
if (present !== names.length)
|
|
1058
1083
|
return false;
|
|
1059
|
-
return
|
|
1084
|
+
return freezeIntact(db, DOCS_FREEZE);
|
|
1060
1085
|
}
|
|
1061
1086
|
/** Install the freeze and change triggers (idempotent). */
|
|
1062
1087
|
function ensureDocsGuards(db) {
|
|
1063
|
-
|
|
1088
|
+
ensureFreeze(db, DOCS_FREEZE);
|
|
1064
1089
|
ensureDocsTracking(db);
|
|
1065
1090
|
}
|
|
1066
1091
|
/** The bare side's change counter, or `undefined` when a change trigger is missing. */
|
|
@@ -1809,8 +1834,198 @@ const AUDIT_LOG = drainPair({
|
|
|
1809
1834
|
rename: { idempotency_key: 'dispatch_idempotency_key' },
|
|
1810
1835
|
savepoint: 'audit_log_row',
|
|
1811
1836
|
});
|
|
1837
|
+
// ── playbook_runs + playbook_approvals ───────────────────────────────────────
|
|
1838
|
+
/** The bare playbook tables, each with its twin and key column. */
|
|
1839
|
+
const PLAYBOOK_TABLES = [
|
|
1840
|
+
{ bare: 'playbook_runs', twin: 'tasks_playbook_runs', key: 'run_id' },
|
|
1841
|
+
{ bare: 'playbook_approvals', twin: 'tasks_playbook_approvals', key: 'approval_id' },
|
|
1842
|
+
];
|
|
1843
|
+
/** Key recording the version of the build that first froze the bare playbook tables. */
|
|
1844
|
+
export const PLAYBOOKS_FROZEN_BY_KEY = 'twin_collapse_frozen_by:playbook_runs';
|
|
1845
|
+
/** Twin rows the initial collapse replaced by their bare rows, as `<bare>\t<id>` → twin JSON. */
|
|
1846
|
+
export const PLAYBOOKS_ARCHIVE_KEY = 'twin_collapse_archive_rows:playbook_runs';
|
|
1847
|
+
/**
|
|
1848
|
+
* The message an older build's write to a frozen bare playbook table aborts
|
|
1849
|
+
* with. Same rules as {@link docsFrozenMessage}: it names the upgrade and
|
|
1850
|
+
* none of the {@link DOCS_FROZEN_FORBIDDEN} phrases.
|
|
1851
|
+
*
|
|
1852
|
+
* @param version - The release that froze the store, or `null`.
|
|
1853
|
+
* @returns The abort message.
|
|
1854
|
+
* @task T12535
|
|
1855
|
+
*/
|
|
1856
|
+
export function playbooksFrozenMessage(version) {
|
|
1857
|
+
const needs = version === null ? 'the latest cleo' : `cleo ${version} or newer`;
|
|
1858
|
+
return (`CLEO: playbook runs moved to tasks_playbook_runs (T12535); this project needs ${needs} ` +
|
|
1859
|
+
'to run playbooks and record approvals. Run: npm i -g @cleocode/cleo@latest');
|
|
1860
|
+
}
|
|
1861
|
+
const PLAYBOOKS_FREEZE = {
|
|
1862
|
+
tables: PLAYBOOK_TABLES.map((t) => t.bare),
|
|
1863
|
+
byKey: PLAYBOOKS_FROZEN_BY_KEY,
|
|
1864
|
+
message: playbooksFrozenMessage,
|
|
1865
|
+
};
|
|
1866
|
+
/** Rows of both sides of one playbook table, by key, over the columns both hold. */
|
|
1867
|
+
function playbookSides(db, t) {
|
|
1868
|
+
const cols = sharedColumns(db, t.bare, t.twin);
|
|
1869
|
+
if (!cols.includes(t.key))
|
|
1870
|
+
return null;
|
|
1871
|
+
return {
|
|
1872
|
+
cols,
|
|
1873
|
+
bare: carriedRowsOf(db, 'main', t.bare, cols, [t.key]),
|
|
1874
|
+
twin: carriedRowsOf(db, 'main', t.twin, cols, [t.key]),
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
function playbookHashes(db) {
|
|
1878
|
+
const out = {};
|
|
1879
|
+
for (const t of PLAYBOOK_TABLES) {
|
|
1880
|
+
const cols = sharedColumns(db, t.bare, t.twin);
|
|
1881
|
+
if (!cols.includes(t.key))
|
|
1882
|
+
continue;
|
|
1883
|
+
for (const [id, row] of carriedRowsOf(db, 'main', t.bare, cols, [t.key]))
|
|
1884
|
+
out[`${t.bare}\t${id}`] = sha(row.json);
|
|
1885
|
+
}
|
|
1886
|
+
return out;
|
|
1887
|
+
}
|
|
1888
|
+
/** Row count and highest rowid of both bare tables: moves on any write to them. */
|
|
1889
|
+
function playbookChangeSeq(db) {
|
|
1890
|
+
try {
|
|
1891
|
+
return PLAYBOOK_TABLES.map((t) => {
|
|
1892
|
+
const r = db.prepare(`SELECT count(*) AS n, max(rowid) AS m FROM main.${t.bare}`).get();
|
|
1893
|
+
return `${r.n}:${r.m ?? 0}`;
|
|
1894
|
+
}).join('/');
|
|
1895
|
+
}
|
|
1896
|
+
catch {
|
|
1897
|
+
return undefined;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Plan the playbook carry. A bare row the twin lacks is carried; an identical
|
|
1902
|
+
* one is left alone. A row both hold with different values: at the initial
|
|
1903
|
+
* collapse the bare row wins (the runtime wrote only the bare tables until
|
|
1904
|
+
* this build, so the twin holds an older exodus copy), the twin's value
|
|
1905
|
+
* archived under {@link PLAYBOOKS_ARCHIVE_KEY}; later (the freeze was missing
|
|
1906
|
+
* while an older build wrote) the twin wins, listed as a conflict.
|
|
1907
|
+
*/
|
|
1908
|
+
function planPlaybooks(db, state) {
|
|
1909
|
+
const plan = emptyPlan();
|
|
1910
|
+
for (const t of PLAYBOOK_TABLES) {
|
|
1911
|
+
const sides = playbookSides(db, t);
|
|
1912
|
+
if (sides === null)
|
|
1913
|
+
continue;
|
|
1914
|
+
for (const [id, row] of sides.bare) {
|
|
1915
|
+
const twin = sides.twin.get(id)?.json;
|
|
1916
|
+
if (twin === row.json)
|
|
1917
|
+
continue;
|
|
1918
|
+
const key = `${t.bare}\t${id}`;
|
|
1919
|
+
if (twin === undefined)
|
|
1920
|
+
plan.set.set(key, row.json);
|
|
1921
|
+
else if (state === undefined) {
|
|
1922
|
+
plan.set.set(key, row.json);
|
|
1923
|
+
plan.archived.push(key);
|
|
1924
|
+
}
|
|
1925
|
+
else
|
|
1926
|
+
plan.conflicts.push(`${key}: kept ${t.twin} (both builds changed it)`);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
return plan;
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Upsert each planned row into its twin in its own savepoint, re-read before
|
|
1933
|
+
* it counts. A row the twin refuses (a CHECK, or the UNIQUE token) is rolled
|
|
1934
|
+
* back, listed as a conflict and stays in the bare table, where the strand
|
|
1935
|
+
* check (`store/sync/flags.ts`) still sees it.
|
|
1936
|
+
*/
|
|
1937
|
+
function applyPlaybooks(db, plan) {
|
|
1938
|
+
let inserted = 0;
|
|
1939
|
+
let replaced = 0;
|
|
1940
|
+
const archive = parseJson(readKv(db, 'tasks_schema_meta', PLAYBOOKS_ARCHIVE_KEY) ?? '{}');
|
|
1941
|
+
const archived = archive !== null && typeof archive === 'object' && !Array.isArray(archive)
|
|
1942
|
+
? archive
|
|
1943
|
+
: {};
|
|
1944
|
+
for (const t of PLAYBOOK_TABLES) {
|
|
1945
|
+
const cols = sharedColumns(db, t.bare, t.twin);
|
|
1946
|
+
if (!cols.includes(t.key))
|
|
1947
|
+
continue;
|
|
1948
|
+
const list = cols.map(quoteIdent).join(', ');
|
|
1949
|
+
const upsert = db.prepare(`INSERT INTO main.${t.twin} (${list}) VALUES (${cols.map(() => '?').join(', ')}) ` +
|
|
1950
|
+
`ON CONFLICT(${quoteIdent(t.key)}) DO UPDATE SET ` +
|
|
1951
|
+
cols.map((c) => `${quoteIdent(c)} = excluded.${quoteIdent(c)}`).join(', '));
|
|
1952
|
+
const twinRow = db.prepare(`SELECT ${list} FROM main.${t.twin} WHERE ${quoteIdent(t.key)} = ?`);
|
|
1953
|
+
const jsonOf = (id) => {
|
|
1954
|
+
const r = twinRow.get(id);
|
|
1955
|
+
return r === undefined ? undefined : rowJson(r, cols);
|
|
1956
|
+
};
|
|
1957
|
+
for (const [key, json] of plan.set) {
|
|
1958
|
+
const [bare, id] = key.split('\t');
|
|
1959
|
+
if (bare !== t.bare)
|
|
1960
|
+
continue;
|
|
1961
|
+
const before = jsonOf(id);
|
|
1962
|
+
db.exec('SAVEPOINT playbook_row');
|
|
1963
|
+
try {
|
|
1964
|
+
upsert.run(...JSON.parse(json));
|
|
1965
|
+
if (jsonOf(id) !== json)
|
|
1966
|
+
// @sync-invariant none:local-only re-reads a row this device's own collapse just wrote; the savepoint rolls it back and the row stays in the bare table
|
|
1967
|
+
throw new Error('the stored row differs');
|
|
1968
|
+
db.exec('RELEASE playbook_row');
|
|
1969
|
+
}
|
|
1970
|
+
catch (error) {
|
|
1971
|
+
db.exec('ROLLBACK TO playbook_row');
|
|
1972
|
+
db.exec('RELEASE playbook_row');
|
|
1973
|
+
plan.conflicts.push(`${key}: not carried (${error instanceof Error ? error.message : error})`);
|
|
1974
|
+
plan.skipped++;
|
|
1975
|
+
continue;
|
|
1976
|
+
}
|
|
1977
|
+
if (before === undefined)
|
|
1978
|
+
inserted++;
|
|
1979
|
+
else {
|
|
1980
|
+
replaced++;
|
|
1981
|
+
archived[key] = before;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
if (replaced > 0)
|
|
1986
|
+
writeKv(db, 'tasks_schema_meta', PLAYBOOKS_ARCHIVE_KEY, JSON.stringify(archived));
|
|
1987
|
+
ensureFreeze(db, PLAYBOOKS_FREEZE);
|
|
1988
|
+
return { inserted, replaced, deleted: 0 };
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* `playbook_runs` + `playbook_approvals` → `tasks_playbook_runs` +
|
|
1992
|
+
* `tasks_playbook_approvals` (T12535), one pair. This build reads and writes
|
|
1993
|
+
* only the twins (`packages/playbooks`), which sync; the bare tables are
|
|
1994
|
+
* carried into them and then FROZEN like the docs pair (option (a), D11160),
|
|
1995
|
+
* so an older build cannot start a run or decide an approval the twins would
|
|
1996
|
+
* never see: it gets {@link playbooksFrozenMessage}.
|
|
1997
|
+
*
|
|
1998
|
+
* - Lossless: no bare row is deleted, and a twin row the initial collapse
|
|
1999
|
+
* replaces is archived, so no snapshot is taken; a failed merge degrades
|
|
2000
|
+
* nothing and is retried at the next open.
|
|
2001
|
+
* - The carry is idempotent: every bare key ends in the twin, so the strand
|
|
2002
|
+
* check passes without accounting; a row the twin refuses stays a strand.
|
|
2003
|
+
* - An unchanged pair costs one `count`/`max(rowid)` read per bare table.
|
|
2004
|
+
*/
|
|
2005
|
+
const PLAYBOOKS = {
|
|
2006
|
+
table: 'playbook_runs',
|
|
2007
|
+
twin: 'tasks_playbook_runs',
|
|
2008
|
+
kvTable: 'tasks_schema_meta',
|
|
2009
|
+
tables: [...PLAYBOOK_TABLES.flatMap((t) => [t.bare, t.twin]), 'tasks_schema_meta'],
|
|
2010
|
+
lossless: true,
|
|
2011
|
+
bareHashes: playbookHashes,
|
|
2012
|
+
// The twins are live and written by this build; only the bare side is compared.
|
|
2013
|
+
twinHashes: () => ({}),
|
|
2014
|
+
changeSeq: playbookChangeSeq,
|
|
2015
|
+
guardsIntact: (db) => freezeIntact(db, PLAYBOOKS_FREEZE),
|
|
2016
|
+
installGuards: (db) => ensureFreeze(db, PLAYBOOKS_FREEZE),
|
|
2017
|
+
plan: planPlaybooks,
|
|
2018
|
+
apply: applyPlaybooks,
|
|
2019
|
+
};
|
|
1812
2020
|
/** The pairs this build collapses, in order. */
|
|
1813
|
-
const PAIRS = [
|
|
2021
|
+
const PAIRS = [
|
|
2022
|
+
SCHEMA_META,
|
|
2023
|
+
STICKY_TAGS,
|
|
2024
|
+
DOCS,
|
|
2025
|
+
TOKEN_USAGE,
|
|
2026
|
+
AUDIT_LOG,
|
|
2027
|
+
PLAYBOOKS,
|
|
2028
|
+
];
|
|
1814
2029
|
/** Connections serving TEMP shadows because their collapse failed. */
|
|
1815
2030
|
const degraded = new WeakMap();
|
|
1816
2031
|
/**
|
|
@@ -1966,7 +2181,9 @@ function collapsePair(db, pair, snapshotPath, dbPath, continuation = false) {
|
|
|
1966
2181
|
throw new Error(`bare ${pair.table} changed after the snapshot decision; retry the open`);
|
|
1967
2182
|
// §2.3a rule 3 (T12775): a merge that wrote a synced twin marks its touch
|
|
1968
2183
|
// set suspect, so the sealer's repair diff covers what capture may miss.
|
|
1969
|
-
|
|
2184
|
+
// Every table of the pair: a pair may write more than one synced twin
|
|
2185
|
+
// (playbooks); only sync-set tables are ever marked.
|
|
2186
|
+
const { result: counts } = withSuspectAccounting(db, hasMainTable(db, 'tasks_tasks') ? 'project' : 'global', pair.tables, () => pair.apply(db, plan));
|
|
1970
2187
|
const now = new Date().toISOString();
|
|
1971
2188
|
const next = {
|
|
1972
2189
|
version: 3,
|