@quolu/lattice 0.12.2 → 0.12.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
4
4
  "description": "Lattice — phase-aware TODO graph compiler and conflict-aware orchestration runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -252,10 +252,13 @@ export async function startBridgeServer({
252
252
  throw new BridgeConfigError(error?.code === 'EADDRINUSE' ? 'BRIDGE_PORT_UNAVAILABLE' : 'BRIDGE_BIND_FAILED',
253
253
  'bridge listen failed', { ...config.listen }, error);
254
254
  });
255
+ const boundAddress = server.address();
256
+ const actualPort = typeof boundAddress === 'object' && boundAddress !== null
257
+ ? boundAddress.port : config.listen.port;
255
258
  let closed = false;
256
259
  return Object.freeze({
257
260
  address: config.listen.address,
258
- port: config.listen.port,
261
+ port: actualPort,
259
262
  updateConfig(next) {
260
263
  if (next?.enabled !== true || next.listen.address !== config.listen.address
261
264
  || next.listen.port !== config.listen.port) {
@@ -27,6 +27,7 @@ import {
27
27
  validateRunRequest,
28
28
  validateRuntimeBoundaryManifest,
29
29
  validateRuntimePlan,
30
+ validRuntimeAbandonReason,
30
31
  verifyRuntimePlanBinding,
31
32
  selfDigest,
32
33
  } from './runtime-contracts.mjs';
@@ -107,7 +108,6 @@ const CLI_ERROR_SCHEMA = 'lattice.cli_error.v2';
107
108
  // 現役run storeは対象Git repo内のLattice-owned・ignored rootへ限定する。
108
109
  const RUN_STORE_ROOT = ['.lattice', 'runs'];
109
110
  const RUN_REF = /^\.lattice\/runs\/([0-9A-Za-z](?:[0-9A-Za-z._-]{0,127}))$/u;
110
- const ABANDON_REASON = /^[0-9A-Za-z](?:[0-9A-Za-z._:-]{0,127})$/u;
111
111
  const KNOWN_ADAPTERS = Object.freeze(['scripted', 'isolated-worktree', 'actual-agent']);
112
112
 
113
113
  class CliContractError extends Error {
@@ -866,8 +866,11 @@ async function runClose({ runDir, repoRoot, stdout, requestId = null }) {
866
866
  }
867
867
 
868
868
  async function runAbandon({ runDir, runRef, reason, stdout, requestId = null }) {
869
- if (!ABANDON_REASON.test(reason)) {
870
- throw new CliContractError('INVALID_ABANDON_REASON', 'reasonは128文字以下の識別子でなければならない');
869
+ if (!validRuntimeAbandonReason(reason)) {
870
+ throw new CliContractError(
871
+ 'INVALID_ABANDON_REASON',
872
+ 'reasonは前後空白・制御文字を含まない1〜256文字の説明でなければならない',
873
+ );
871
874
  }
872
875
  return withLifecycleLock(runDir, async () => {
873
876
  const current = await readRunStore(runDir);
@@ -2126,7 +2129,11 @@ export async function runManagedSupervisorDaemon({
2126
2129
  const active = await readCommittedEpochStore(runDir);
2127
2130
  const state = projectRuntimeState({ events });
2128
2131
  const shutdownReason = controlRequest.payload.shutdown_reason;
2129
- if (typeof shutdownReason !== 'string' || shutdownReason.length === 0) {
2132
+ if (controlRequest.operation === 'abandon' && !validRuntimeAbandonReason(shutdownReason)) {
2133
+ throw new ManagedRuntimeError('INVALID_ABANDON_REASON', 'abandon reasonが監査文字列規律を満たさない');
2134
+ }
2135
+ if (controlRequest.operation === 'close'
2136
+ && (typeof shutdownReason !== 'string' || shutdownReason.length === 0)) {
2130
2137
  throw new ManagedRuntimeError('MANAGED_SHUTDOWN_INCOMPLETE', 'shutdown reason不足');
2131
2138
  }
2132
2139
  let proposed;
@@ -2789,7 +2796,7 @@ export async function runRuntimeCli({ argv, cwd, stdout, stderr }) {
2789
2796
  } else if (argv.length === 6
2790
2797
  && argv[0] === 'run' && argv[1] === 'abandon'
2791
2798
  && argv[2] === '--run' && typeof argv[3] === 'string' && argv[3].length > 0
2792
- && argv[4] === '--reason' && typeof argv[5] === 'string' && argv[5].length > 0) {
2799
+ && argv[4] === '--reason' && typeof argv[5] === 'string') {
2793
2800
  action = async () => {
2794
2801
  const { runDir } = await resolveRunStore(cwd, argv[3]);
2795
2802
  return runAbandon({ runDir, runRef: argv[3], reason: argv[5], stdout,
@@ -12,6 +12,19 @@ const IDENTIFIER = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
12
12
  const MAX_COLLECTION = 256;
13
13
  const MAX_NODES_PER_PLAN = 8;
14
14
 
15
+ // 人間向けaudit reasonは表示を偽装できる制御文字を拒否しつつ、通常のUnicode説明を保持する。
16
+ const AUDIT_BIDI_CONTROLS = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u;
17
+
18
+ export function validRuntimeAbandonReason(value) {
19
+ return typeof value === 'string'
20
+ && value === value.trim()
21
+ && [...value].length >= 1
22
+ && [...value].length <= 256
23
+ && !/\p{Cc}/u.test(value)
24
+ && !/[\u2028\u2029]/u.test(value)
25
+ && !AUDIT_BIDI_CONTROLS.test(value);
26
+ }
27
+
15
28
  // ADR 0044 Decision 3.2のclosed event kind set。拡張はrun_event.v2+新ADRでだけ行う。
16
29
  export const RUN_EVENT_KINDS = Object.freeze([
17
30
  'run_initialized',
@@ -1,5 +1,10 @@
1
1
  import { canonicalizeArtifact, digestArtifact } from './artifact-contracts.mjs';
2
- import { selfDigest, validateEpochRebindPacket, validateExecutorPacket } from './runtime-contracts.mjs';
2
+ import {
3
+ selfDigest,
4
+ validRuntimeAbandonReason,
5
+ validateEpochRebindPacket,
6
+ validateExecutorPacket,
7
+ } from './runtime-contracts.mjs';
3
8
 
4
9
  const SHA256 = /^[0-9a-f]{64}$/;
5
10
  const ID = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
@@ -247,9 +252,11 @@ function validateRuntimeControlOperation(value) {
247
252
  ? digest(value.checkpoint_digest) : value.checkpoint_digest === null)
248
253
  && Number.isSafeInteger(value.expected_epoch) && value.expected_epoch > 0
249
254
  && nullableDigest(value.expected_queue_digest)
250
- && (['close', 'abandon'].includes(value.operation)
251
- ? typeof value.shutdown_reason === 'string' && value.shutdown_reason.length > 0
252
- : value.shutdown_reason === null)
255
+ && (value.operation === 'abandon'
256
+ ? validRuntimeAbandonReason(value.shutdown_reason)
257
+ : value.operation === 'close'
258
+ ? typeof value.shutdown_reason === 'string' && value.shutdown_reason.length > 0
259
+ : value.shutdown_reason === null)
253
260
  && selfValid(value, 'operation_digest');
254
261
  }
255
262
 
@@ -250,6 +250,14 @@ function affectedPayload(raw, expectPath) {
250
250
  return plainRecord(payload) ? payload : null;
251
251
  }
252
252
 
253
+ function affectedTarget(raw, expectPath) {
254
+ if (!plainRecord(raw) || !Array.isArray(raw.targets)) return null;
255
+ const entry = raw.targets.find((candidate) => (
256
+ plainRecord(candidate) && candidate.target === expectPath
257
+ ));
258
+ return plainRecord(entry) ? entry : null;
259
+ }
260
+
253
261
  function rawEntries(raw) {
254
262
  const payload = rawPayload(raw);
255
263
  return Array.isArray(payload) ? payload : null;
@@ -269,6 +277,7 @@ function resolveBindingStatus(binding, outcome) {
269
277
  if (outcome.status !== 'ready') return outcome.status;
270
278
  const { expect } = binding;
271
279
  if (expect.kind === 'affected') {
280
+ if (affectedTarget(outcome.raw, expect.path)?.path_state === 'absent') return 'path_absent';
272
281
  const payload = affectedPayload(outcome.raw, expect.path);
273
282
  if (payload === null
274
283
  || !Array.isArray(payload.changedFiles)
@@ -676,7 +685,29 @@ export function compileRuntimePlanV1(options = {}) {
676
685
  return nonDispatchable('NODE_LIMIT_EXCEEDED', { todo_count: todoIds.length });
677
686
  }
678
687
  if (compiled.outcome === 'unknown') {
679
- return nonDispatchable('BOUNDARY_UNKNOWN', { unknowns: compiled.unknowns });
688
+ const hasFreshAbsentPath = todoIds.some((todoId) => (
689
+ bindingsByTodo.get(todoId).some((binding) => {
690
+ if (!['path', 'affected'].includes(binding.expect.kind)) return false;
691
+ const query = queryById.get(binding.query_id);
692
+ const outcome = outcomeByQueryId.get(binding.query_id);
693
+ return query.operation === 'affected'
694
+ && outcome.status === 'empty'
695
+ && affectedTarget(outcome.raw, binding.expect.path)?.path_state === 'absent';
696
+ })
697
+ ));
698
+ return nonDispatchable('BOUNDARY_UNKNOWN', {
699
+ unknowns: compiled.unknowns,
700
+ unresolved_witnesses: unknowns,
701
+ guidance: hasFreshAbsentPath
702
+ ? {
703
+ code: 'BOOTSTRAP_OWNERSHIP_SEAM',
704
+ message: 'fresh path観測で不存在の新規pathは親が空の専用seamをbase commitへ先行追加し、sensor sync後に同じrequestを再compileする',
705
+ }
706
+ : {
707
+ code: 'ACQUIRE_OWNERSHIP_EVIDENCE',
708
+ message: '既存path・symbol・未束縛ownershipはfresh Sensor queryを追加して同じrequestを再compileする',
709
+ },
710
+ });
680
711
  }
681
712
  if (compiled.outcome === 'unsupported' && compiled.code === 'SEARCH_BUDGET_EXHAUSTED') {
682
713
  return nonDispatchable('SEARCH_BUDGET_EXHAUSTED', {});
@@ -291,6 +291,7 @@ async function collectOne({ cwd, query, execute, inspectAffectedPath }) {
291
291
  results.push({
292
292
  target: targetPath,
293
293
  outcome: 'empty',
294
+ path_state: 'absent',
294
295
  data: emptyAffectedData(targetPath),
295
296
  });
296
297
  continue;
package/src/todo-cli.mjs CHANGED
@@ -38,7 +38,7 @@ import {
38
38
  readTodoStore,
39
39
  readTodoStoreStable,
40
40
  rebuildTodoSnapshot,
41
- verifyPhaseTodoRevisionSources,
41
+ verifyEffectivePhaseTodoRevisionSources,
42
42
  verifyTodoRevisionSources,
43
43
  } from './todo-store.mjs';
44
44
  import {
@@ -803,6 +803,7 @@ async function ensureActiveProjectDashboard({ repoRoot, env }) {
803
803
  async function verify({ repoRoot, requestedPlanKey }) {
804
804
  const store = await readTodoStore({ repoRoot });
805
805
  const members = selectMembers(store, requestedPlanKey);
806
+ const verifiedSourceInventories = new Map();
806
807
  for (const member of members) {
807
808
  const unverified = member.tasks.find((task) => task.evidence_unverified);
808
809
  if (unverified !== undefined) {
@@ -816,12 +817,14 @@ async function verify({ repoRoot, requestedPlanKey }) {
816
817
  case 'lattice.todo_revision.v1':
817
818
  case 'lattice.todo_revision.v2':
818
819
  await verifyTodoRevisionSources({ repoRoot, revision: member.revision });
820
+ verifiedSourceInventories.set(member.descriptor.plan_key,
821
+ member.revision.source_inventory);
819
822
  break;
820
823
  case 'lattice.phase_todo_revision.v1':
821
824
  case 'lattice.phase_todo_revision.v2':
822
- break;
823
825
  case 'lattice.phase_todo_revision.v3':
824
- await verifyPhaseTodoRevisionSources({ repoRoot, revision: member.revision });
826
+ verifiedSourceInventories.set(member.descriptor.plan_key,
827
+ await verifyEffectivePhaseTodoRevisionSources({ repoRoot, member }));
825
828
  break;
826
829
  default:
827
830
  throw new TodoStoreError('REVISION_INVALID', 'revision_schema_or_digest_invalid');
@@ -830,6 +833,7 @@ async function verify({ repoRoot, requestedPlanKey }) {
830
833
  }
831
834
  const verifiedMembers = members.map((member) => {
832
835
  const reconciled = member.revision !== null;
836
+ const sourceInventory = verifiedSourceInventories.get(member.descriptor.plan_key) ?? null;
833
837
  const phaseRevision = ['lattice.phase_todo_revision.v1', 'lattice.phase_todo_revision.v2',
834
838
  'lattice.phase_todo_revision.v3']
835
839
  .includes(member.revision?.schema);
@@ -848,15 +852,11 @@ async function verify({ repoRoot, requestedPlanKey }) {
848
852
  : todoLegacyReconciliationDigest({ planDigest: member.plan.plan_digest,
849
853
  journalHeadDigest: member.journal.events.at(-1).event_digest }),
850
854
  source_inventory_count: reconciled
851
- ? phaseRevision && member.revision.schema !== 'lattice.phase_todo_revision.v3' ? 0
852
- : member.revision.source_inventory.active.length
853
- + member.revision.source_inventory.excluded_tombstones.length : null,
855
+ ? sourceInventory.active.length + sourceInventory.excluded_tombstones.length : null,
854
856
  active_task_count: reconciled
855
- ? phaseRevision && member.revision.schema !== 'lattice.phase_todo_revision.v3'
856
- ? 0 : member.revision.source_inventory.active.length : null,
857
+ ? sourceInventory.active.length : null,
857
858
  excluded_tombstone_count: reconciled
858
- ? phaseRevision && member.revision.schema !== 'lattice.phase_todo_revision.v3'
859
- ? 0 : member.revision.source_inventory.excluded_tombstones.length : null,
859
+ ? sourceInventory.excluded_tombstones.length : null,
860
860
  };
861
861
  });
862
862
  const result = {
@@ -2552,6 +2552,21 @@ async function predecessorSourceInventory(repoRoot, previous) {
2552
2552
  return { active: [], excluded_tombstones: [] };
2553
2553
  }
2554
2554
 
2555
+ export async function verifyEffectivePhaseTodoRevisionSources(options = {}) {
2556
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
2557
+ const member = options.member;
2558
+ const revision = member?.revision;
2559
+ if (!validatePhaseTodoRevision(revision)
2560
+ || canonicalizeTodoArtifact(revision.desired_plan)
2561
+ !== canonicalizeTodoArtifact(member?.plan)) {
2562
+ fail('REVISION_INVALID', 'phase_revision_schema_or_digest_invalid');
2563
+ }
2564
+ const inventory = revision.schema === 'lattice.phase_todo_revision.v3'
2565
+ ? revision.source_inventory : await predecessorSourceInventory(repoRoot, member);
2566
+ await verifyRevisionSources(repoRoot, inventory);
2567
+ return inventory;
2568
+ }
2569
+
2555
2570
  function validatePhaseV3SourceInventoryDiff(previousInventory, revision) {
2556
2571
  const desired = revision.source_inventory;
2557
2572
  const previousActive = new Map(previousInventory.active.map((entry) => [entry.task_id, entry]));
@@ -2655,7 +2670,9 @@ async function applyPhaseTodoRevisionV3(options, revision, repoRoot) {
2655
2670
  journalHeadDigest: previous.journal.events.at(-1).event_digest });
2656
2671
  if (revision.reconciliation.predecessor_reconciliation_digest
2657
2672
  !== predecessorReconciliationDigest) fail('STORE_WRITE_CONFLICT', 'stale_predecessor');
2658
- validatePhaseV3SourceInventoryDiff(await predecessorSourceInventory(repoRoot, previous), revision);
2673
+ const previousInventory = await predecessorSourceInventory(repoRoot, previous);
2674
+ await verifyRevisionSources(repoRoot, previousInventory);
2675
+ validatePhaseV3SourceInventoryDiff(previousInventory, revision);
2659
2676
  await rejectCompetingPhaseV3Transaction(repoRoot, revision);
2660
2677
  verifyPlanNarrativeAnchors(repoRoot, revision.desired_plan, previous.plan);
2661
2678
  const stateMigration = stateMigrationFor(previous, revision);