@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 +1 -1
- package/src/bridge-server.mjs +4 -1
- package/src/runtime-cli.mjs +12 -5
- package/src/runtime-contracts.mjs +13 -0
- package/src/runtime-controller-protocol.mjs +11 -4
- package/src/runtime-front-end.mjs +32 -1
- package/src/sensor-adapter.mjs +1 -0
- package/src/todo-cli.mjs +10 -10
- package/src/todo-store.mjs +18 -1
package/package.json
CHANGED
package/src/bridge-server.mjs
CHANGED
|
@@ -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:
|
|
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) {
|
package/src/runtime-cli.mjs
CHANGED
|
@@ -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 (!
|
|
870
|
-
throw new CliContractError(
|
|
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 (
|
|
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'
|
|
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 {
|
|
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
|
-
&& (
|
|
251
|
-
?
|
|
252
|
-
: value.
|
|
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
|
-
|
|
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', {});
|
package/src/sensor-adapter.mjs
CHANGED
package/src/todo-cli.mjs
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
readTodoStore,
|
|
39
39
|
readTodoStoreStable,
|
|
40
40
|
rebuildTodoSnapshot,
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
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
|
-
?
|
|
856
|
-
? 0 : member.revision.source_inventory.active.length : null,
|
|
857
|
+
? sourceInventory.active.length : null,
|
|
857
858
|
excluded_tombstone_count: reconciled
|
|
858
|
-
?
|
|
859
|
-
? 0 : member.revision.source_inventory.excluded_tombstones.length : null,
|
|
859
|
+
? sourceInventory.excluded_tombstones.length : null,
|
|
860
860
|
};
|
|
861
861
|
});
|
|
862
862
|
const result = {
|
package/src/todo-store.mjs
CHANGED
|
@@ -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
|
-
|
|
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);
|