@quolu/lattice 0.12.34 → 0.14.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.
@@ -20,6 +20,10 @@ import {
20
20
  validateTodoPlan,
21
21
  validateTodoSnapshot,
22
22
  } from './todo-contracts.mjs';
23
+ import {
24
+ validateTodoIndependence,
25
+ validateTodoWitnessSet,
26
+ } from './todo-independence-contracts.mjs';
23
27
  import { sha256Bytes, verifyLinearHashChain } from './hash-chain.mjs';
24
28
  import {
25
29
  parseTodoSourceRef,
@@ -3533,3 +3537,131 @@ export async function createSuccessorTodoPlan(options = {}) {
3533
3537
  return { plan, genesis, snapshot };
3534
3538
  });
3535
3539
  }
3540
+
3541
+ const INDEPENDENCE_ARTIFACT_NAME = 'independence.json';
3542
+ const INDEPENDENCE_ARTIFACT_BYTES = 1_048_576;
3543
+
3544
+ /**
3545
+ * planと同じversionディレクトリに置く並置artifactのref(ADR 0127 Decision 1)。
3546
+ * manifestへは登録しない。plan versionが変われば旧artifactは自然に非アクティブになる。
3547
+ */
3548
+ export function todoIndependenceRef(planKey, planVersion) {
3549
+ return `${STORE_ROOT_REF}/plans/${planKey}/${planVersion}/${INDEPENDENCE_ARTIFACT_NAME}`;
3550
+ }
3551
+
3552
+ function activeMember(store, planKey) {
3553
+ const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey);
3554
+ if (!member) fail('STORE_INCONSISTENT', 'plan_not_active', { plan_key: planKey });
3555
+ return member;
3556
+ }
3557
+
3558
+ /**
3559
+ * independence artifactを、active planへbindしてから書く。
3560
+ *
3561
+ * plan versionとtopology digestが現在のactive planと一致しない記録は、書いた瞬間から
3562
+ * 別topologyについての主張になるため受理しない。planの正本(journal・snapshot・manifest)
3563
+ * には触れない。
3564
+ */
3565
+ export async function writeTodoIndependenceArtifact(options = {}) {
3566
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
3567
+ const { artifact } = options;
3568
+ if (!validateTodoIndependence(artifact)) {
3569
+ fail('INDEPENDENCE_ARTIFACT_INVALID', 'independence_artifact_invalid');
3570
+ }
3571
+ return withLock(repoRoot, async () => {
3572
+ const store = await readTodoStore({ repoRoot, forWrite: true, now: options.now });
3573
+ if (store.project_id !== artifact.project_id) {
3574
+ fail('INDEPENDENCE_BINDING_MISMATCH', 'project_id_mismatch', {
3575
+ expected: store.project_id, actual: artifact.project_id,
3576
+ });
3577
+ }
3578
+ const member = activeMember(store, artifact.plan_key);
3579
+ if (member.plan.plan_version !== artifact.plan_version) {
3580
+ fail('INDEPENDENCE_BINDING_MISMATCH', 'plan_version_mismatch', {
3581
+ expected: member.plan.plan_version, actual: artifact.plan_version,
3582
+ });
3583
+ }
3584
+ if (member.plan.topology_digest !== artifact.topology_digest) {
3585
+ fail('INDEPENDENCE_BINDING_MISMATCH', 'topology_digest_mismatch', {
3586
+ expected: member.plan.topology_digest, actual: artifact.topology_digest,
3587
+ });
3588
+ }
3589
+ const planTaskIds = new Set(member.plan.tasks.map(({ task_id: taskId }) => taskId));
3590
+ const absent = artifact.task_ids.filter((taskId) => !planTaskIds.has(taskId));
3591
+ if (absent.length > 0) {
3592
+ fail('INDEPENDENCE_BINDING_MISMATCH', 'task_absent_from_plan', { task_ids: absent });
3593
+ }
3594
+ const ref = todoIndependenceRef(artifact.plan_key, artifact.plan_version);
3595
+ await atomicWrite(path.resolve(repoRoot, ref), canonicalLine(artifact));
3596
+ return { ref, artifact };
3597
+ });
3598
+ }
3599
+
3600
+ /**
3601
+ * active planに紐づくindependence artifactを読む。
3602
+ *
3603
+ * 記録が無ければnull(「まだ判定していない」)を返す。壊れている・非canonical・
3604
+ * 契約違反はnullへ丸めずtyped failにする。無い状態と読めない状態を同じ顔にしない。
3605
+ */
3606
+ export async function readTodoIndependenceArtifact(options = {}) {
3607
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
3608
+ const store = options.store ?? await readTodoStore({ repoRoot, now: options.now });
3609
+ const member = activeMember(store, options.planKey);
3610
+ const ref = todoIndependenceRef(member.plan.plan_key, member.plan.plan_version);
3611
+ try {
3612
+ return await readArtifact(repoRoot, ref, {
3613
+ code: 'INDEPENDENCE_ARTIFACT_INVALID',
3614
+ maxBytes: INDEPENDENCE_ARTIFACT_BYTES,
3615
+ validate: validateTodoIndependence,
3616
+ missing: true,
3617
+ });
3618
+ } catch (error) {
3619
+ // 読めない記録は握りつぶさない。ただしどのplanをどう直すかまで言わないと、
3620
+ // 消費者は「壊れている」以上のことができない。
3621
+ if (error instanceof TodoStoreError && error.code === 'INDEPENDENCE_ARTIFACT_INVALID') {
3622
+ throw new TodoStoreError(error.code, error.detail.reason, undefined, {
3623
+ ...error.detail,
3624
+ plan_key: member.plan.plan_key,
3625
+ artifact_ref: ref,
3626
+ next_action: 'recompile_independence_or_remove_stale_record',
3627
+ });
3628
+ }
3629
+ throw error;
3630
+ }
3631
+ }
3632
+
3633
+ const WITNESS_SET_BYTES = 4_194_304;
3634
+
3635
+ /**
3636
+ * witness set宣言の置き場(ADR 0128 Decision 6)。
3637
+ *
3638
+ * 運用規約だった規則をコードの所有へ移す。`todoIndependenceRef`が判定結果のpathを持つのに対し、
3639
+ * こちらは入力のpathを持つ。plan versionで分けないのは、宣言はtopologyでなくtaskについての
3640
+ * 記述であり、revisionを跨いで移行して使い続けるためである。
3641
+ */
3642
+ export function todoWitnessRef(planKey) {
3643
+ return `${STORE_ROOT_REF}/witness/${planKey}.json`;
3644
+ }
3645
+
3646
+ /** witness setを読む。無ければnull、壊れていればtyped fail(両者を同じ顔にしない)。 */
3647
+ export async function readTodoWitnessSet(options = {}) {
3648
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
3649
+ return readArtifact(repoRoot, todoWitnessRef(options.planKey), {
3650
+ code: 'INVALID_TODO_WITNESS_SET',
3651
+ maxBytes: WITNESS_SET_BYTES,
3652
+ validate: validateTodoWitnessSet,
3653
+ missing: true,
3654
+ });
3655
+ }
3656
+
3657
+ /** witness setを書く。canonical JSON+LFで、契約を満たさないものは書かせない。 */
3658
+ export async function writeTodoWitnessSet(options = {}) {
3659
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
3660
+ const { witnessSet } = options;
3661
+ if (!validateTodoWitnessSet(witnessSet)) {
3662
+ fail('INVALID_TODO_WITNESS_SET', 'witness_set_invalid');
3663
+ }
3664
+ const ref = todoWitnessRef(witnessSet.plan_key);
3665
+ await atomicWrite(path.resolve(repoRoot, ref), canonicalLine(witnessSet));
3666
+ return { ref, witnessSet };
3667
+ }