@quolu/lattice 0.12.3 → 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.3",
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",
@@ -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;