@quolu/lattice 0.50.1 → 0.52.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.
- package/bin/lattice-work-order-adapter.mjs +20 -0
- package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -0
- package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -0
- package/package.json +5 -2
- package/src/boundary-observation-compiler-v2.mjs +1 -1
- package/src/cli-help.mjs +33 -2
- package/src/rc3-actual-dogfood.mjs +6 -2
- package/src/rc3-scripted-campaign.mjs +37 -10
- package/src/rc4-stage1-dogfood.mjs +6 -2
- package/src/runtime-adapter-registry.mjs +21 -7
- package/src/runtime-cli.mjs +476 -34
- package/src/runtime-contracts.mjs +59 -13
- package/src/runtime-controller-protocol.mjs +48 -3
- package/src/runtime-decision-verifier.mjs +70 -0
- package/src/runtime-diff-observer.mjs +66 -4
- package/src/runtime-direct-os-observer.mjs +25 -8
- package/src/runtime-driver-state.mjs +162 -0
- package/src/runtime-engine.mjs +37 -6
- package/src/runtime-front-end.mjs +39 -1
- package/src/runtime-managed-supervisor.mjs +80 -14
- package/src/runtime-multi-epoch-store.mjs +87 -14
- package/src/runtime-pull-intake.mjs +1188 -0
- package/src/runtime-work-order-contracts.mjs +91 -0
- package/src/runtime-work-order-controller.mjs +1167 -0
- package/src/seam-proposal-queries.mjs +1 -1
- package/src/todo-cli.mjs +273 -7
- package/src/todo-contracts.mjs +19 -2
- package/src/todo-gantt-html-independence.mjs +3 -2
- package/src/todo-gantt-html-shared.mjs +1 -2
- package/src/todo-gantt-html-style.mjs +13 -0
- package/src/todo-gantt-html.mjs +15 -2
- package/src/todo-gantt-layout.mjs +75 -1
- package/src/todo-gantt-nested.mjs +263 -0
- package/src/todo-gantt-svg.mjs +80 -5
- package/src/todo-independence-contracts.mjs +73 -7
- package/src/todo-independence-guidance.mjs +30 -1
- package/src/todo-independence.mjs +89 -7
- package/src/todo-revision.mjs +1 -1
- package/src/todo-split.mjs +472 -0
- package/src/todo-status.mjs +10 -1
- package/src/todo-store-git-transaction.mjs +418 -0
- package/src/todo-store.mjs +144 -4
|
@@ -5,7 +5,7 @@ import { SENSOR_QUERY_OPERATIONS } from './runtime-contracts.mjs';
|
|
|
5
5
|
import { collectSensorEvidence, portableSensorOutcome } from './sensor-adapter.mjs';
|
|
6
6
|
import { todoSelfDigest } from './todo-contracts.mjs';
|
|
7
7
|
|
|
8
|
-
const CONFLICT_KINDS = new Set(['symbol', 'path', 'state', 'effect']);
|
|
8
|
+
const CONFLICT_KINDS = new Set(['symbol', 'path', 'state', 'effect', 'line']);
|
|
9
9
|
const QUERYABLE_KINDS = new Set(['symbol', 'path']);
|
|
10
10
|
const SYMBOL_OPERATIONS = Object.freeze(['query', 'callers', 'callees', 'impact']);
|
|
11
11
|
const SENSOR_OPERATIONS = new Set(SENSOR_QUERY_OPERATIONS);
|
package/src/todo-cli.mjs
CHANGED
|
@@ -46,10 +46,12 @@ import {
|
|
|
46
46
|
createTodoStoreWriter,
|
|
47
47
|
TodoStoreError,
|
|
48
48
|
isPhaselessTodoPlanSchema,
|
|
49
|
+
projectTodoCrossPlanDependencies,
|
|
49
50
|
TERMINAL_AUDIT_PHASE_ID,
|
|
50
51
|
readTodoIndependenceArtifact,
|
|
51
52
|
readTodoSeamProposalArtifact,
|
|
52
53
|
readTodoStore,
|
|
54
|
+
resolveTodoStartRetractionBinding,
|
|
53
55
|
readTodoWitnessSet,
|
|
54
56
|
todoWitnessRef,
|
|
55
57
|
writeTodoWitnessSet,
|
|
@@ -60,6 +62,7 @@ import {
|
|
|
60
62
|
verifyEffectivePhaseTodoRevisionSources,
|
|
61
63
|
verifyTodoRevisionSources,
|
|
62
64
|
} from './todo-store.mjs';
|
|
65
|
+
import { withStartRetractionGuard } from './runtime-pull-intake.mjs';
|
|
63
66
|
import {
|
|
64
67
|
appendTodoExtraction,
|
|
65
68
|
compileTodoExtraction,
|
|
@@ -94,6 +97,7 @@ import {
|
|
|
94
97
|
selectIndependenceGuidance,
|
|
95
98
|
selectWitnessScaffoldGuidance,
|
|
96
99
|
selectSeamProposalGuidance,
|
|
100
|
+
scopeExpansionRecommendations,
|
|
97
101
|
} from './todo-independence-guidance.mjs';
|
|
98
102
|
import {
|
|
99
103
|
buildSeamProposalQuerySet,
|
|
@@ -116,6 +120,10 @@ import {
|
|
|
116
120
|
parseTodoSourceRef, todoLegacyReconciliationDigest, validatePhaseTodoRevision,
|
|
117
121
|
validateTodoRevision, validateTodoRevisionSet,
|
|
118
122
|
} from './todo-revision.mjs';
|
|
123
|
+
import {
|
|
124
|
+
compileTodoSplit,
|
|
125
|
+
prepareTodoSplitWitnessMigration,
|
|
126
|
+
} from './todo-split.mjs';
|
|
119
127
|
import {
|
|
120
128
|
appendTodoNote,
|
|
121
129
|
readTodoNoteContext,
|
|
@@ -124,6 +132,7 @@ import {
|
|
|
124
132
|
readTodoPlanNotesForStatus,
|
|
125
133
|
} from './todo-note-store.mjs';
|
|
126
134
|
import { readTodoParallelCandidatesForStatus } from './todo-parallel-candidates.mjs';
|
|
135
|
+
import { commitTodoStoreMutation } from './todo-store-git-transaction.mjs';
|
|
127
136
|
|
|
128
137
|
const CLI_ERROR_SCHEMA = 'lattice.cli_error.v2';
|
|
129
138
|
const DEFAULT_GANTT_SCOPE = 'live';
|
|
@@ -177,7 +186,7 @@ function typedFailure(stderr, error) {
|
|
|
177
186
|
const TODO_COMMAND_NAMES = Object.freeze([
|
|
178
187
|
'status', 'show', 'note', 'bindings', 'independence', 'seam-profile', 'seam-proposal',
|
|
179
188
|
'verify', 'snapshot', 'gantt', 'dashboard', 'phase', 'migrate', 'start', 'block',
|
|
180
|
-
'unblock', 'done', 'reopen', 'evidence', 'revise', 'revise-phase', 'revise-set',
|
|
189
|
+
'unblock', 'done', 'reopen', 'evidence', 'split', 'revise', 'revise-phase', 'revise-set',
|
|
181
190
|
]);
|
|
182
191
|
|
|
183
192
|
function typedArgumentFailure(stderr, code, message, detail) {
|
|
@@ -186,6 +195,30 @@ function typedArgumentFailure(stderr, code, message, detail) {
|
|
|
186
195
|
return 2;
|
|
187
196
|
}
|
|
188
197
|
|
|
198
|
+
function supportsAtomicStoreCommit(argv) {
|
|
199
|
+
const command = argv[0];
|
|
200
|
+
if (command === 'note') return argv[1] !== 'list';
|
|
201
|
+
if (command === 'independence') {
|
|
202
|
+
return argv[1] === 'mode'
|
|
203
|
+
|| (argv[1] === 'witness' && ['migrate', 'scaffold'].includes(argv[2]));
|
|
204
|
+
}
|
|
205
|
+
if (command === 'snapshot') return argv[1] === '--rebuild';
|
|
206
|
+
if (command === 'migrate') return !argv.includes('--dry-run') && !argv.includes('--schema');
|
|
207
|
+
if (['revise', 'split', 'revise-set', 'revise-phase', 'start', 'retract', 'block',
|
|
208
|
+
'unblock', 'done', 'reopen'].includes(command)) return true;
|
|
209
|
+
if (command === 'evidence') return argv[1] === 'promote';
|
|
210
|
+
if (command === 'phase') return argv[1] !== 'status';
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function atomicStoreCommitUnsupported(stderr, argv) {
|
|
215
|
+
return typedArgumentFailure(stderr, 'STORE_COMMIT_UNSUPPORTED',
|
|
216
|
+
'todo_command_does_not_mutate_only_the_store', {
|
|
217
|
+
command: argv.slice(0, 3),
|
|
218
|
+
next_action: 'remove_--commit-store_or_use_a_supported_todo_write_command',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
189
222
|
function resolveRepoRoot(cwd) {
|
|
190
223
|
try {
|
|
191
224
|
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
@@ -249,9 +282,13 @@ function taskRef(plan, taskId) {
|
|
|
249
282
|
}
|
|
250
283
|
|
|
251
284
|
function mergedTopology(store) {
|
|
285
|
+
const crossPlanDependencies = projectTodoCrossPlanDependencies(store.members);
|
|
252
286
|
return {
|
|
253
287
|
nodes: store.members.flatMap(({ plan }) => plan.tasks.map(({ task_id: taskId }) => taskRef(plan, taskId))),
|
|
254
|
-
hard_edges:
|
|
288
|
+
hard_edges: [
|
|
289
|
+
...store.members.flatMap(({ plan }) => plan.hard_dependencies),
|
|
290
|
+
...crossPlanDependencies.map(({ from, to }) => ({ from, to })),
|
|
291
|
+
],
|
|
255
292
|
joins: store.members.flatMap(({ plan }) => plan.joins),
|
|
256
293
|
};
|
|
257
294
|
}
|
|
@@ -609,6 +646,7 @@ async function startAdvisory({ repoRoot, store, projection, planKey, taskId }) {
|
|
|
609
646
|
coverage: 'missing',
|
|
610
647
|
drift_intersecting: null,
|
|
611
648
|
conflicts_with_active: [],
|
|
649
|
+
scope_expansion_recommendations: [],
|
|
612
650
|
uncovered_active_task_ids: projection.active_set
|
|
613
651
|
.filter((task) => task.plan_key === planKey).map(({ task_id: id }) => id),
|
|
614
652
|
self_unknowns: [{ kind: 'witness_missing', ref: 'no_independence_record' }],
|
|
@@ -652,6 +690,9 @@ async function startAdvisory({ repoRoot, store, projection, planKey, taskId }) {
|
|
|
652
690
|
drift_intersecting: projected.drift === null
|
|
653
691
|
? null : projected.drift.intersecting_task_ids.includes(taskId),
|
|
654
692
|
conflicts_with_active: conflictsWithActive,
|
|
693
|
+
scope_expansion_recommendations: scopeExpansionRecommendations(
|
|
694
|
+
Array.isArray(artifact.scope_expanded) ? artifact.scope_expanded : [],
|
|
695
|
+
).filter(({ task_id: expandedTaskId }) => expandedTaskId === taskId),
|
|
655
696
|
uncovered_active_task_ids: projected.uncovered_active_task_ids,
|
|
656
697
|
self_unknowns: selfUnknowns,
|
|
657
698
|
guidance: selectIndependenceGuidance({
|
|
@@ -786,6 +827,23 @@ async function startTask({
|
|
|
786
827
|
payload: { override_reason: overrideReason }, evidenceRef: null, advisory, noteContext });
|
|
787
828
|
}
|
|
788
829
|
|
|
830
|
+
async function retractStart({ repoRoot, env, planKey, taskId, reason }) {
|
|
831
|
+
const actor = mutationActor(env);
|
|
832
|
+
const store = await readTodoStore({ repoRoot });
|
|
833
|
+
const binding = resolveTodoStartRetractionBinding(store, { planKey, taskId, actor });
|
|
834
|
+
return withStartRetractionGuard({
|
|
835
|
+
repoRoot,
|
|
836
|
+
planKey,
|
|
837
|
+
taskId: binding.task_id,
|
|
838
|
+
activationEventDigest: binding.activation_event_digest,
|
|
839
|
+
action: () => mutate({
|
|
840
|
+
repoRoot, env, planKey, taskId: binding.task_id, kind: 'start_retracted',
|
|
841
|
+
payload: { reason, target_start_digest: binding.activation_event_digest },
|
|
842
|
+
evidenceRef: null,
|
|
843
|
+
}),
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
|
|
789
847
|
function validatePhaseDecisionInput(value, outcome) {
|
|
790
848
|
const keys = outcome === 'accept'
|
|
791
849
|
? ['schema', 'review_event_digest', 'decision_evidence', 'evidence_slots', 'input_digest']
|
|
@@ -864,6 +922,57 @@ async function independenceMode({ repoRoot, env, planKey, mode, reason }) {
|
|
|
864
922
|
return result;
|
|
865
923
|
}
|
|
866
924
|
|
|
925
|
+
/** 開発中に発見したplan跨ぎ依存を、consumer planのplan-scoped chainへ接続する。 */
|
|
926
|
+
async function dependencyConnect({
|
|
927
|
+
repoRoot, env, fromPlanKey, fromTaskId, toPlanKey, toTaskId, reason,
|
|
928
|
+
}) {
|
|
929
|
+
const store = await readTodoStore({ repoRoot });
|
|
930
|
+
const dependencyMember = (planKey) => {
|
|
931
|
+
const member = store.members.find(({ plan }) => plan.plan_key === planKey);
|
|
932
|
+
if (member === undefined) {
|
|
933
|
+
throw new TodoStoreError('DEPENDENCY_INVALID', 'dependency_plan_not_found', undefined, {
|
|
934
|
+
plan_key: planKey,
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
return member;
|
|
938
|
+
};
|
|
939
|
+
const source = dependencyMember(fromPlanKey);
|
|
940
|
+
const target = dependencyMember(toPlanKey);
|
|
941
|
+
if (!source.plan.tasks.some(({ task_id: taskId }) => taskId === fromTaskId)) {
|
|
942
|
+
throw new TodoStoreError('DEPENDENCY_INVALID', 'dependency_task_not_found', undefined, {
|
|
943
|
+
plan_key: fromPlanKey, task_id: fromTaskId,
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
if (!target.plan.tasks.some(({ task_id: taskId }) => taskId === toTaskId)) {
|
|
947
|
+
throw new TodoStoreError('DEPENDENCY_INVALID', 'dependency_task_not_found', undefined, {
|
|
948
|
+
plan_key: toPlanKey, task_id: toTaskId,
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
const from = {
|
|
952
|
+
project_id: store.project_id, plan_key: fromPlanKey, task_id: fromTaskId,
|
|
953
|
+
expected_topology_digest: source.plan.topology_digest,
|
|
954
|
+
};
|
|
955
|
+
const to = {
|
|
956
|
+
project_id: store.project_id, plan_key: toPlanKey, task_id: toTaskId,
|
|
957
|
+
expected_topology_digest: target.plan.topology_digest,
|
|
958
|
+
};
|
|
959
|
+
const { event } = await appendTodoEvent({
|
|
960
|
+
repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }), planKey: toPlanKey,
|
|
961
|
+
event: {
|
|
962
|
+
kind: 'cross_plan_dependency', actor: mutationActor(env), payload: { from, to, reason },
|
|
963
|
+
},
|
|
964
|
+
});
|
|
965
|
+
const result = {
|
|
966
|
+
schema: 'lattice.todo_dependency_connect_result.v1', project_id: store.project_id,
|
|
967
|
+
from: event.payload.from, to: event.payload.to, reason: event.payload.reason,
|
|
968
|
+
connected_by: event.actor, connected_at: event.recorded_at,
|
|
969
|
+
event_digest: event.event_digest, plan_scoped_head_digest: event.event_digest,
|
|
970
|
+
result_digest: '',
|
|
971
|
+
};
|
|
972
|
+
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
973
|
+
return result;
|
|
974
|
+
}
|
|
975
|
+
|
|
867
976
|
async function phaseStatus({ repoRoot, planKey }) {
|
|
868
977
|
const store = await readTodoStore({ repoRoot });
|
|
869
978
|
const [member] = selectMembers(store, planKey);
|
|
@@ -1322,6 +1431,69 @@ async function revisePhase({ repoRoot, env, planKey, inputRef }) {
|
|
|
1322
1431
|
revision, actor: mutationActor(env), recordedAt: new Date().toISOString() });
|
|
1323
1432
|
}
|
|
1324
1433
|
|
|
1434
|
+
async function splitTodo({ repoRoot, env, planKey, inputRef }) {
|
|
1435
|
+
const proposal = await readMigrationInput(repoRoot, inputRef, { requireValid: false });
|
|
1436
|
+
const store = await readTodoStore({ repoRoot });
|
|
1437
|
+
const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey);
|
|
1438
|
+
if (member === undefined) throw new TodoStoreError('STORE_INCONSISTENT', 'plan_not_active');
|
|
1439
|
+
const witnessSet = await readTodoWitnessSet({ repoRoot, planKey });
|
|
1440
|
+
if (witnessSet === null) {
|
|
1441
|
+
throw new TodoStoreError('WITNESS_MIGRATION_UNAVAILABLE', 'witness_set_absent', undefined, {
|
|
1442
|
+
witness_ref: todoWitnessRef(planKey),
|
|
1443
|
+
next_action: `lattice todo independence witness scaffold --plan ${planKey} --input <draft>`,
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
const compiled = await compileTodoSplit({ repoRoot, member, proposal });
|
|
1447
|
+
const actor = mutationActor(env);
|
|
1448
|
+
const recordedAt = new Date().toISOString();
|
|
1449
|
+
// splitのmigrationは既存taskへのidentity写像だけである。宣言を純粋に移行・検査し、
|
|
1450
|
+
// 同じcanonical bytesをwitness先へ書けることまでapply前に確定する。これにより、
|
|
1451
|
+
// witness失敗をrevision適用後に返してplan/sourceだけ進んだ状態を作らない。
|
|
1452
|
+
const preparedWitness = prepareTodoSplitWitnessMigration({
|
|
1453
|
+
witnessSet, revision: compiled.revision,
|
|
1454
|
+
});
|
|
1455
|
+
const { ref: witnessRef } = await writeTodoWitnessSet({
|
|
1456
|
+
repoRoot, witnessSet: preparedWitness.witnessSet,
|
|
1457
|
+
});
|
|
1458
|
+
const witnessMigration = {
|
|
1459
|
+
schema: 'lattice.todo_witness_migrate_result.v1',
|
|
1460
|
+
project_id: store.project_id,
|
|
1461
|
+
plan_key: planKey,
|
|
1462
|
+
plan_version: compiled.revision.desired_plan.plan_version,
|
|
1463
|
+
witness_ref: witnessRef,
|
|
1464
|
+
migrated_count: preparedWitness.migrated_count,
|
|
1465
|
+
removed_count: preparedWitness.removed_count,
|
|
1466
|
+
unchanged_count: preparedWitness.unchanged_count,
|
|
1467
|
+
witness_set_digest: preparedWitness.witnessSet.witness_set_digest,
|
|
1468
|
+
result_digest: '',
|
|
1469
|
+
};
|
|
1470
|
+
witnessMigration.result_digest = todoSelfDigest(witnessMigration, 'result_digest');
|
|
1471
|
+
const receipt = compiled.revision.schema === 'lattice.phase_todo_revision.v3'
|
|
1472
|
+
? await applyPhaseTodoRevision({
|
|
1473
|
+
repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }),
|
|
1474
|
+
revision: compiled.revision, actor, recordedAt,
|
|
1475
|
+
})
|
|
1476
|
+
: await applyTodoRevision({
|
|
1477
|
+
repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }),
|
|
1478
|
+
revision: compiled.revision, actor, recordedAt,
|
|
1479
|
+
});
|
|
1480
|
+
const result = {
|
|
1481
|
+
schema: 'lattice.todo_split_result.v1',
|
|
1482
|
+
project_id: store.project_id,
|
|
1483
|
+
plan_key: planKey,
|
|
1484
|
+
predecessor_task_id: proposal.task_id,
|
|
1485
|
+
residual_task_id: proposal.task_id,
|
|
1486
|
+
extracted_task_ids: compiled.extracted_task_ids,
|
|
1487
|
+
plan_version: compiled.revision.desired_plan.plan_version,
|
|
1488
|
+
revision_digest: compiled.revision.revision_digest,
|
|
1489
|
+
revision_receipt_digest: receipt.receipt_digest ?? receipt.result_digest,
|
|
1490
|
+
witness_migration_result_digest: witnessMigration.result_digest,
|
|
1491
|
+
result_digest: '',
|
|
1492
|
+
};
|
|
1493
|
+
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
1494
|
+
return result;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1325
1497
|
async function status({ repoRoot }) {
|
|
1326
1498
|
const store = await readTodoStore({ repoRoot });
|
|
1327
1499
|
return projectTodoStatus(store, {
|
|
@@ -1530,17 +1702,23 @@ async function independenceCompile({ repoRoot, planKey, inputRef }) {
|
|
|
1530
1702
|
const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey);
|
|
1531
1703
|
if (!member) throw new TodoStoreError('STORE_INCONSISTENT', 'plan_not_active', undefined, { plan_key: planKey });
|
|
1532
1704
|
|
|
1705
|
+
// 前回artifactを渡して膨張の履歴を継ぐ。**例外を握り潰さない。**
|
|
1706
|
+
// `readTodoIndependenceArtifact` は「欠落だけnull・旧版はlegacy marker・壊れた記録は
|
|
1707
|
+
// INDEPENDENCE_ARTIFACT_INVALIDでtyped fail」を既に区別している。ここでcatchすると
|
|
1708
|
+
// **corrupt/permission/I-Oまで「初回」へ化けて履歴が黙って切れる**(suzune の監査で実測・room [1148])。
|
|
1709
|
+
const previousArtifact = await readTodoIndependenceArtifact({ repoRoot, store, planKey });
|
|
1533
1710
|
const artifact = compileTodoIndependence({
|
|
1534
1711
|
witnessSet,
|
|
1535
1712
|
plan: member.plan,
|
|
1536
1713
|
baseSha,
|
|
1537
1714
|
compiledAt: new Date().toISOString(),
|
|
1538
1715
|
sensorEvidence: await collectWitnessSensorEvidence({ cwd: repoRoot, witnessSet }),
|
|
1716
|
+
previousArtifact,
|
|
1539
1717
|
});
|
|
1540
1718
|
const { ref } = await writeTodoIndependenceArtifact({ repoRoot, artifact });
|
|
1541
1719
|
|
|
1542
1720
|
const result = {
|
|
1543
|
-
schema: 'lattice.todo_independence_compile_result.
|
|
1721
|
+
schema: 'lattice.todo_independence_compile_result.v2',
|
|
1544
1722
|
project_id: artifact.project_id,
|
|
1545
1723
|
plan_key: artifact.plan_key,
|
|
1546
1724
|
plan_version: artifact.plan_version,
|
|
@@ -1550,6 +1728,7 @@ async function independenceCompile({ repoRoot, planKey, inputRef }) {
|
|
|
1550
1728
|
task_count: artifact.task_ids.length,
|
|
1551
1729
|
conflict_count: artifact.conflicts.length,
|
|
1552
1730
|
unknown_count: artifact.unknowns.length,
|
|
1731
|
+
scope_expansion_recommendations: scopeExpansionRecommendations(artifact.scope_expanded),
|
|
1553
1732
|
result_digest: '',
|
|
1554
1733
|
};
|
|
1555
1734
|
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
@@ -2218,7 +2397,35 @@ async function independenceForGantt({ repoRoot, store }) {
|
|
|
2218
2397
|
const projections = [];
|
|
2219
2398
|
for (const member of store.members) {
|
|
2220
2399
|
const planKey = member.plan.plan_key;
|
|
2221
|
-
|
|
2400
|
+
let artifact = null;
|
|
2401
|
+
let unreadableReason = null;
|
|
2402
|
+
try {
|
|
2403
|
+
artifact = await readTodoIndependenceArtifact({ repoRoot, store, planKey });
|
|
2404
|
+
} catch (error) {
|
|
2405
|
+
if (!(error instanceof TodoStoreError)) throw error;
|
|
2406
|
+
unreadableReason = `${error.code}:${error.detail?.reason ?? error.message}`;
|
|
2407
|
+
}
|
|
2408
|
+
if (unreadableReason !== null) {
|
|
2409
|
+
if (currentBaseSha === null) currentBaseSha = currentHeadSha(repoRoot);
|
|
2410
|
+
const projected = projectIndependenceFrontier({
|
|
2411
|
+
artifact: null,
|
|
2412
|
+
readyTaskIds: frontier.filter((task) => task.plan_key === planKey)
|
|
2413
|
+
.map(({ task_id: taskId }) => taskId),
|
|
2414
|
+
activeTaskIds: status.active_set.filter((task) => task.plan_key === planKey)
|
|
2415
|
+
.map(({ task_id: taskId }) => taskId),
|
|
2416
|
+
plan: member.plan,
|
|
2417
|
+
currentBaseSha,
|
|
2418
|
+
changedPaths: null,
|
|
2419
|
+
});
|
|
2420
|
+
projections.push({
|
|
2421
|
+
project_id: member.plan.project_id,
|
|
2422
|
+
plan_key: planKey,
|
|
2423
|
+
coverage: 'unreadable',
|
|
2424
|
+
unreadable_reason: unreadableReason,
|
|
2425
|
+
frontier: projected.frontier,
|
|
2426
|
+
});
|
|
2427
|
+
continue;
|
|
2428
|
+
}
|
|
2222
2429
|
if (artifact === null) continue;
|
|
2223
2430
|
// 記録があるplanが1つでもあれば鮮度の判定にHEADが要る。
|
|
2224
2431
|
if (currentBaseSha === null) currentBaseSha = currentHeadSha(repoRoot);
|
|
@@ -2238,6 +2445,7 @@ async function independenceForGantt({ repoRoot, store }) {
|
|
|
2238
2445
|
project_id: member.plan.project_id,
|
|
2239
2446
|
plan_key: planKey,
|
|
2240
2447
|
coverage: projected.coverage,
|
|
2448
|
+
unreadable_reason: null,
|
|
2241
2449
|
frontier: projected.frontier,
|
|
2242
2450
|
});
|
|
2243
2451
|
}
|
|
@@ -2253,12 +2461,29 @@ async function seamProposalsForGantt({ repoRoot, store }) {
|
|
|
2253
2461
|
const projections = [];
|
|
2254
2462
|
for (const member of store.members) {
|
|
2255
2463
|
const planKey = member.plan.plan_key;
|
|
2256
|
-
|
|
2464
|
+
let artifact = null;
|
|
2465
|
+
try {
|
|
2466
|
+
artifact = await readTodoSeamProposalArtifact({ repoRoot, store, planKey });
|
|
2467
|
+
} catch (error) {
|
|
2468
|
+
if (!(error instanceof TodoStoreError)) throw error;
|
|
2469
|
+
projections.push({
|
|
2470
|
+
project_id: member.plan.project_id,
|
|
2471
|
+
plan_key: planKey,
|
|
2472
|
+
coverage: 'superseded',
|
|
2473
|
+
unreadable_reason: `${error.code}:${error.detail?.reason ?? error.message}`,
|
|
2474
|
+
guidance: selectSeamProposalGuidance({ coverage: 'superseded' }),
|
|
2475
|
+
component_count: null,
|
|
2476
|
+
conflict_resource_count: null,
|
|
2477
|
+
components: [],
|
|
2478
|
+
});
|
|
2479
|
+
continue;
|
|
2480
|
+
}
|
|
2257
2481
|
if (artifact === null) {
|
|
2258
2482
|
projections.push({
|
|
2259
2483
|
project_id: member.plan.project_id,
|
|
2260
2484
|
plan_key: planKey,
|
|
2261
2485
|
coverage: 'missing',
|
|
2486
|
+
unreadable_reason: null,
|
|
2262
2487
|
guidance: selectSeamProposalGuidance({ coverage: 'missing' }),
|
|
2263
2488
|
component_count: null,
|
|
2264
2489
|
conflict_resource_count: null,
|
|
@@ -2268,7 +2493,14 @@ async function seamProposalsForGantt({ repoRoot, store }) {
|
|
|
2268
2493
|
}
|
|
2269
2494
|
|
|
2270
2495
|
if (currentBaseSha === null) currentBaseSha = currentHeadSha(repoRoot);
|
|
2271
|
-
|
|
2496
|
+
let independenceArtifact = null;
|
|
2497
|
+
let unreadableReason = null;
|
|
2498
|
+
try {
|
|
2499
|
+
independenceArtifact = await readTodoIndependenceArtifact({ repoRoot, store, planKey });
|
|
2500
|
+
} catch (error) {
|
|
2501
|
+
if (!(error instanceof TodoStoreError)) throw error;
|
|
2502
|
+
unreadableReason = `${error.code}:${error.detail?.reason ?? error.message}`;
|
|
2503
|
+
}
|
|
2272
2504
|
const binding = artifact.source_binding;
|
|
2273
2505
|
const independenceMatches = independenceArtifact !== null
|
|
2274
2506
|
&& validateTodoIndependence(independenceArtifact)
|
|
@@ -2287,6 +2519,7 @@ async function seamProposalsForGantt({ repoRoot, store }) {
|
|
|
2287
2519
|
project_id: member.plan.project_id,
|
|
2288
2520
|
plan_key: planKey,
|
|
2289
2521
|
coverage,
|
|
2522
|
+
unreadable_reason: unreadableReason,
|
|
2290
2523
|
guidance: selectSeamProposalGuidance({ coverage }),
|
|
2291
2524
|
component_count: components.length,
|
|
2292
2525
|
conflict_resource_count: components
|
|
@@ -2538,6 +2771,13 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
2538
2771
|
throw new TypeError('runTodoCli optionsが不正');
|
|
2539
2772
|
}
|
|
2540
2773
|
|
|
2774
|
+
const atomicCommit = argv.at(-1) === '--commit-store';
|
|
2775
|
+
if (atomicCommit) argv = argv.slice(0, -1);
|
|
2776
|
+
if (atomicCommit && ((argv[1] === '--schema' && argv[2] === '--json')
|
|
2777
|
+
|| (argv[0] === 'dashboard' && argv[1] === 'remove'))) {
|
|
2778
|
+
return atomicStoreCommitUnsupported(stderr, argv);
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2541
2781
|
if (argv[0] === 'migrate' && argv[1] === '--input'
|
|
2542
2782
|
&& typeof argv[2] === 'string' && path.isAbsolute(argv[2])) {
|
|
2543
2783
|
return typedArgumentFailure(stderr, 'INPUT_OUTSIDE_REPOSITORY', 'absolute_input_path_rejected', {
|
|
@@ -2645,6 +2885,16 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
2645
2885
|
action = (repoRoot) => independenceMode({
|
|
2646
2886
|
repoRoot, env, planKey: argv[3], mode: argv[5], reason: argv[7],
|
|
2647
2887
|
});
|
|
2888
|
+
} else if (argv.length === 12 && argv[0] === 'dependency' && argv[1] === 'connect'
|
|
2889
|
+
&& argv[2] === '--from-plan' && isTodoIdentifier(argv[3])
|
|
2890
|
+
&& argv[4] === '--from-task' && isTodoIdentifier(argv[5])
|
|
2891
|
+
&& argv[6] === '--to-plan' && isTodoIdentifier(argv[7])
|
|
2892
|
+
&& argv[8] === '--to-task' && isTodoIdentifier(argv[9])
|
|
2893
|
+
&& argv[10] === '--reason' && argv[11].length > 0) {
|
|
2894
|
+
action = (repoRoot) => dependencyConnect({
|
|
2895
|
+
repoRoot, env, fromPlanKey: argv[3], fromTaskId: argv[5],
|
|
2896
|
+
toPlanKey: argv[7], toTaskId: argv[9], reason: argv[11],
|
|
2897
|
+
});
|
|
2648
2898
|
} else if (argv.length === 6 && argv[0] === 'independence' && argv[1] === 'compile'
|
|
2649
2899
|
&& argv[2] === '--plan' && isTodoIdentifier(argv[3]) && argv[4] === '--input') {
|
|
2650
2900
|
action = (repoRoot) => independenceCompile({
|
|
@@ -2731,6 +2981,10 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
2731
2981
|
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
2732
2982
|
&& argv[3] === '--input' && isTodoRef(argv[4])) {
|
|
2733
2983
|
action = (repoRoot) => revise({ repoRoot, env, planKey: argv[2], inputRef: argv[4] });
|
|
2984
|
+
} else if (argv.length === 5 && argv[0] === 'split'
|
|
2985
|
+
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
2986
|
+
&& argv[3] === '--input' && isTodoRef(argv[4])) {
|
|
2987
|
+
action = (repoRoot) => splitTodo({ repoRoot, env, planKey: argv[2], inputRef: argv[4] });
|
|
2734
2988
|
} else if (argv.length === 3 && argv[0] === 'revise-set'
|
|
2735
2989
|
&& argv[1] === '--input' && isTodoRef(argv[2])) {
|
|
2736
2990
|
action = (repoRoot) => reviseSet({ repoRoot, env, inputRef: argv[2] });
|
|
@@ -2784,6 +3038,13 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
2784
3038
|
action = (repoRoot) => startTask({ repoRoot, env, planKey: argv[2], taskId: argv[4],
|
|
2785
3039
|
overrideReason, parallelFrontier: argv.length === 6,
|
|
2786
3040
|
serialConfirmed: argv.length === 8 });
|
|
3041
|
+
} else if (argv.length === 7 && argv[0] === 'retract'
|
|
3042
|
+
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
3043
|
+
&& argv[3] === '--task' && isTodoIdentifier(argv[4])
|
|
3044
|
+
&& argv[5] === '--reason' && argv[6].length > 0) {
|
|
3045
|
+
action = (repoRoot) => retractStart({
|
|
3046
|
+
repoRoot, env, planKey: argv[2], taskId: argv[4], reason: argv[6],
|
|
3047
|
+
});
|
|
2787
3048
|
} else if (argv.length === 7 && argv[0] === 'block'
|
|
2788
3049
|
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
2789
3050
|
&& argv[3] === '--task' && isTodoIdentifier(argv[4])
|
|
@@ -2831,6 +3092,9 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
2831
3092
|
command, next_action: argumentHelp,
|
|
2832
3093
|
});
|
|
2833
3094
|
}
|
|
3095
|
+
if (atomicCommit && !supportsAtomicStoreCommit(argv)) {
|
|
3096
|
+
return atomicStoreCommitUnsupported(stderr, argv);
|
|
3097
|
+
}
|
|
2834
3098
|
|
|
2835
3099
|
try {
|
|
2836
3100
|
const repoRoot = resolveRepoRoot(cwd);
|
|
@@ -2840,7 +3104,9 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
2840
3104
|
if (!ganttCommand && !dashboardAdopt && !migrationDryRun) {
|
|
2841
3105
|
await ensureActiveProjectDashboard({ repoRoot, env });
|
|
2842
3106
|
}
|
|
2843
|
-
const result =
|
|
3107
|
+
const result = atomicCommit
|
|
3108
|
+
? await commitTodoStoreMutation({ repoRoot, argv, action, env })
|
|
3109
|
+
: await action(repoRoot);
|
|
2844
3110
|
if (result !== null) stdout.write(`${JSON.stringify(result)}\n`);
|
|
2845
3111
|
return 0;
|
|
2846
3112
|
} catch (error) {
|
package/src/todo-contracts.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { isCanonicalUtcTimestamp } from './timestamp-contract.mjs';
|
|
3
3
|
|
|
4
4
|
export const TODO_EVENT_KINDS = Object.freeze([
|
|
5
|
-
'plan_genesis', 'start', 'block', 'unblock', 'done', 'reopen',
|
|
5
|
+
'plan_genesis', 'start', 'start_retracted', 'block', 'unblock', 'done', 'reopen',
|
|
6
6
|
'phase_review', 'phase_accept', 'phase_reject', 'phase_reopen',
|
|
7
7
|
// ADR 0148: 監査していない歴史を「監査なしで閉じた」として明示的に閉じるための専用kind。
|
|
8
8
|
// phase_review/accept/reject/reopenと同じv3 tail event shape(phase_id持ち)に収め、
|
|
@@ -13,10 +13,15 @@ export const TODO_EVENT_KINDS = Object.freeze([
|
|
|
13
13
|
// 「誰が選んだか」の帰属を持つ——witnessが全planの暗黙義務だった時に帰属が無く、正確な
|
|
14
14
|
// 案内が素通りされたことへの是正である(オーナー裁定C①)。
|
|
15
15
|
'coordination_mode',
|
|
16
|
+
// 開発中に発見したplan跨ぎの依存を、active plan topologyの追記改変ではなく
|
|
17
|
+
// version-boundなplan-scoped eventとして接続する。推定はせず、AIが発見した時だけ積む。
|
|
18
|
+
'cross_plan_dependency',
|
|
16
19
|
]);
|
|
17
20
|
|
|
18
21
|
/** planへ帰属し、taskにもPhaseにも属さないevent kind。 */
|
|
19
|
-
export const TODO_PLAN_SCOPED_EVENT_KINDS = Object.freeze([
|
|
22
|
+
export const TODO_PLAN_SCOPED_EVENT_KINDS = Object.freeze([
|
|
23
|
+
'coordination_mode', 'cross_plan_dependency',
|
|
24
|
+
]);
|
|
20
25
|
|
|
21
26
|
/** 調整方式。witness=独立性を宣言し検証して並列する/conversation=会話で調整する。 */
|
|
22
27
|
export const TODO_COORDINATION_MODES = Object.freeze(['witness', 'conversation']);
|
|
@@ -506,7 +511,19 @@ function validPayload(event) {
|
|
|
506
511
|
&& TODO_COORDINATION_MODES.includes(payload.mode)
|
|
507
512
|
&& nullableText(payload.reason) && payload.reason !== null;
|
|
508
513
|
}
|
|
514
|
+
if (event.kind === 'cross_plan_dependency') {
|
|
515
|
+
const dependencyRef = (value) => exactRecord(value, [
|
|
516
|
+
'project_id', 'plan_key', 'task_id', 'expected_topology_digest',
|
|
517
|
+
]) && isTodoIdentifier(value.project_id) && isTodoIdentifier(value.plan_key)
|
|
518
|
+
&& isTodoIdentifier(value.task_id) && isTodoDigest(value.expected_topology_digest);
|
|
519
|
+
return exactRecord(payload, ['from', 'to', 'reason'])
|
|
520
|
+
&& dependencyRef(payload.from) && dependencyRef(payload.to)
|
|
521
|
+
&& nullableText(payload.reason) && payload.reason !== null;
|
|
522
|
+
}
|
|
509
523
|
if (event.kind === 'start') return exactRecord(payload, ['override_reason']) && nullableText(payload.override_reason);
|
|
524
|
+
if (event.kind === 'start_retracted') return exactRecord(payload, ['reason', 'target_start_digest'])
|
|
525
|
+
&& nullableText(payload.reason) && payload.reason !== null
|
|
526
|
+
&& isTodoDigest(payload.target_start_digest);
|
|
510
527
|
if (event.kind === 'block') return exactRecord(payload, ['reason']) && nullableText(payload.reason) && payload.reason !== null;
|
|
511
528
|
if (event.kind === 'unblock') return exactRecord(payload, []);
|
|
512
529
|
if (event.kind === 'done' && payload?.done_mode === 'authored') {
|
|
@@ -139,7 +139,8 @@ export function renderRightPane(
|
|
|
139
139
|
) {
|
|
140
140
|
const lookup = presentationLookup(presentation);
|
|
141
141
|
const sectionByKey = new Map(sections.map((section) => [refKey(section.ref), section]));
|
|
142
|
-
const
|
|
142
|
+
const semanticNodes = [...layout.nodes, ...(layout.hierarchy_nodes ?? [])];
|
|
143
|
+
const nodeByKey = new Map(semanticNodes.map((node) => [refKey(node.ref), node]));
|
|
143
144
|
const folds = foldIndex(layout);
|
|
144
145
|
const incoming = new Map(sections.map((section) => [refKey(section.ref), []]));
|
|
145
146
|
const outgoing = new Map(sections.map((section) => [refKey(section.ref), []]));
|
|
@@ -163,7 +164,7 @@ export function renderRightPane(
|
|
|
163
164
|
const counts = { pending: 0, 'in-progress': 0, blocked: 0, done: 0 };
|
|
164
165
|
for (const section of sections) counts[section.state.status] += 1;
|
|
165
166
|
const active = sections.filter((section) => section.state.status === 'in-progress');
|
|
166
|
-
const ready =
|
|
167
|
+
const ready = semanticNodes.filter((node) => node.visibility.next_ready);
|
|
167
168
|
const independenceSummary = summarizeIndependence(layout);
|
|
168
169
|
const readyHeadline = ready.length > 1
|
|
169
170
|
? `<p class="readiness-note"><strong>同時dispatch推奨:</strong> ${ready.length}工程。${escapeHtmlText(dispatchBasis(independenceSummary))}</p>`
|
|
@@ -90,8 +90,7 @@ export function presentationLookup(presentation) {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
export function taskReference(section, lookup) {
|
|
93
|
-
|
|
94
|
-
return number === undefined ? `ID ${section.task.task_id}` : `工程 ${number.display_number}`;
|
|
93
|
+
return `工程 ${section.task.task_id}`;
|
|
95
94
|
}
|
|
96
95
|
|
|
97
96
|
export function renderRelationList(relations, sectionByKey, lookup, emptyText, folds = new Set()) {
|
|
@@ -116,3 +116,16 @@ button.fold-chip[aria-expanded="true"]{border-color:var(--text-primary)}
|
|
|
116
116
|
.lane-dimmed{opacity:.35}
|
|
117
117
|
@media(max-width:900px){body{display:block;height:auto}.shell{display:block}.pane-divider{display:none}.gantt-pane,.narrative-pane{height:70vh}.gantt-pane{border-bottom:1px solid var(--border)}}
|
|
118
118
|
`;
|
|
119
|
+
|
|
120
|
+
// 階層を持つplanだけが読み込む。親無しplanのHTML/CSS bytesを変えないため、基底CSSへは混ぜない。
|
|
121
|
+
export const NESTED_CSS = `
|
|
122
|
+
.nested-task-panel{filter:drop-shadow(0 4px 12px rgba(11,11,11,.18))}
|
|
123
|
+
.nested-task-surface{fill:var(--surface-1);stroke:var(--text-secondary);stroke-width:1.5}
|
|
124
|
+
.nested-task-label{fill:var(--text-primary);font-size:12px;font-weight:650}
|
|
125
|
+
.nested-task-link{fill:none;stroke:var(--text-secondary);stroke-width:1.5;stroke-dasharray:4 3}
|
|
126
|
+
.nested-task-diagram{outline:1px solid var(--border);background:var(--surface-1)}
|
|
127
|
+
.nested-task-toggle{cursor:pointer}
|
|
128
|
+
.nested-task-toggle rect{fill:var(--surface-1);stroke:var(--text-secondary);stroke-width:1.5}
|
|
129
|
+
.nested-task-toggle text{fill:var(--text-primary);font-size:14px;font-weight:650}
|
|
130
|
+
.nested-task-toggle:focus rect{stroke:var(--text-primary);stroke-width:2.5}
|
|
131
|
+
`;
|
package/src/todo-gantt-html.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { serializeJsonForScript } from './todo-markdown-renderer.mjs';
|
|
|
5
5
|
import { renderTodoGanttSvg, TODO_GANTT_STATUS_PRESENTATION } from './todo-gantt-svg.mjs';
|
|
6
6
|
import { renderDiagramLegend, renderRightPane } from './todo-gantt-html-independence.mjs';
|
|
7
7
|
import { escapeHtmlAttribute, escapeHtmlText, refKey } from './todo-gantt-html-shared.mjs';
|
|
8
|
-
import { CSS } from './todo-gantt-html-style.mjs';
|
|
8
|
+
import { CSS, NESTED_CSS } from './todo-gantt-html-style.mjs';
|
|
9
9
|
|
|
10
10
|
export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.v19';
|
|
11
11
|
export const TODO_GANTT_PROSE_MAX_BYTES = 8 * 1024 * 1024;
|
|
@@ -188,6 +188,18 @@ const CONTROLLER = `
|
|
|
188
188
|
})();
|
|
189
189
|
`;
|
|
190
190
|
|
|
191
|
+
const NESTED_CONTROLLER = `
|
|
192
|
+
(()=>{
|
|
193
|
+
const root=document.querySelector('[data-gantt-root]');if(!root)return;
|
|
194
|
+
const toggles=[...root.querySelectorAll('[data-nested-toggle-for]')];
|
|
195
|
+
const panels=[...root.querySelectorAll('[data-nested-panel-for]')];
|
|
196
|
+
const panelFor=(key)=>panels.find(panel=>panel.dataset.nestedPanelFor===key);
|
|
197
|
+
const toggle=(control)=>{const key=control.dataset.nestedToggleFor;const panel=panelFor(key);if(!panel)return;const open=panel.hasAttribute('hidden');panel.toggleAttribute('hidden',!open);const link=[...root.querySelectorAll('[data-nested-link-for]')].find(candidate=>candidate.dataset.nestedLinkFor===key);link?.toggleAttribute('hidden',!open);control.setAttribute('aria-expanded',String(open));const mark=control.querySelector('text');if(mark)mark.textContent=open?'−':'+';};
|
|
198
|
+
root.addEventListener('click',event=>{const control=event.target.closest('[data-nested-toggle-for]');if(!control||!root.contains(control))return;event.preventDefault();event.stopPropagation();toggle(control);});
|
|
199
|
+
root.addEventListener('keydown',event=>{const control=event.target.closest('[data-nested-toggle-for]');if(!control||!root.contains(control)||(event.key!=='Enter'&&event.key!==' '))return;event.preventDefault();event.stopPropagation();toggle(control);});
|
|
200
|
+
})();
|
|
201
|
+
`;
|
|
202
|
+
|
|
191
203
|
export function renderTodoGanttHtml({
|
|
192
204
|
readModel, layout, narratives = [], anchorOutcomes = [], presentation = null, metadata = {},
|
|
193
205
|
expandedLayout = null, noteContexts = null, noteWarnings = [],
|
|
@@ -208,6 +220,7 @@ export function renderTodoGanttHtml({
|
|
|
208
220
|
}
|
|
209
221
|
const normalized = normalizeSections(readModel, narratives, anchorOutcomes, noteContexts);
|
|
210
222
|
const displayName = projectDisplayName(readModel, metadata);
|
|
223
|
+
const hasHierarchy = layout?.hierarchy?.schema === 'lattice.todo_gantt_hierarchy.v1';
|
|
211
224
|
const svg = renderTodoGanttSvg(layout, { presentation });
|
|
212
225
|
// The expanded diagram travels with the page so the badge can bring the
|
|
213
226
|
// history back without a round trip. A file:// artifact has nowhere to ask.
|
|
@@ -224,7 +237,7 @@ export function renderTodoGanttHtml({
|
|
|
224
237
|
metadata,
|
|
225
238
|
presentation,
|
|
226
239
|
});
|
|
227
|
-
const html = `<!doctype html><html lang="ja"><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Lattice — ${escapeHtmlText(displayName)} 依存工程図</title><style>${CSS}</style></head><body data-gantt-root data-view-state="overview"><main class="shell"><section class="gantt-pane" aria-label="${escapeHtmlAttribute(displayName)} 依存工程図"><div class="diagram-toolbar" role="group" aria-label="図のズーム"><strong class="project-heading">${escapeHtmlText(displayName)} 依存工程図</strong>${renderAuditPendingChip(readModel)}<button type="button" data-zoom-action="out" aria-label="縮小">−</button><button type="button" data-zoom-action="reset">等倍</button><button type="button" data-zoom-action="in" aria-label="拡大">+</button><button type="button" data-zoom-action="fit">全体表示</button><output class="zoom-readout" data-zoom-output aria-live="polite">100%</output><span class="diagram-note">縦=依存段階(時間ではない)</span></div>${renderDiagramLegend(presentation, layout, expandedSvg !== '')}<div class="diagram-scroll" data-diagram-scroll tabindex="0" aria-label="縦方向を主にスクロール可能な依存工程図">${diagrams}</div></section><div class="pane-divider" data-pane-divider aria-hidden="true"></div><aside class="narrative-pane" aria-label="選択工程の詳細と全工程一覧">${rightPane}</aside></main><script type="application/json" id="todo-gantt-data">${staticData}</script><script>${CONTROLLER}</script></body></html>`;
|
|
240
|
+
const html = `<!doctype html><html lang="ja"><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Lattice — ${escapeHtmlText(displayName)} 依存工程図</title><style>${CSS}${hasHierarchy ? NESTED_CSS : ''}</style></head><body data-gantt-root data-view-state="overview"><main class="shell"><section class="gantt-pane" aria-label="${escapeHtmlAttribute(displayName)} 依存工程図"><div class="diagram-toolbar" role="group" aria-label="図のズーム"><strong class="project-heading">${escapeHtmlText(displayName)} 依存工程図</strong>${renderAuditPendingChip(readModel)}<button type="button" data-zoom-action="out" aria-label="縮小">−</button><button type="button" data-zoom-action="reset">等倍</button><button type="button" data-zoom-action="in" aria-label="拡大">+</button><button type="button" data-zoom-action="fit">全体表示</button><output class="zoom-readout" data-zoom-output aria-live="polite">100%</output><span class="diagram-note">縦=依存段階(時間ではない)</span></div>${renderDiagramLegend(presentation, layout, expandedSvg !== '')}<div class="diagram-scroll" data-diagram-scroll tabindex="0" aria-label="縦方向を主にスクロール可能な依存工程図">${diagrams}</div></section><div class="pane-divider" data-pane-divider aria-hidden="true"></div><aside class="narrative-pane" aria-label="選択工程の詳細と全工程一覧">${rightPane}</aside></main><script type="application/json" id="todo-gantt-data">${staticData}</script><script>${CONTROLLER}${hasHierarchy ? NESTED_CONTROLLER : ''}</script></body></html>`;
|
|
228
241
|
const htmlBytes = Buffer.byteLength(html, 'utf8');
|
|
229
242
|
if (htmlBytes > TODO_GANTT_HTML_MAX_BYTES) {
|
|
230
243
|
throw new TodoGanttRenderError('TODO_SCALE_EXCEEDED', 'todo gantt HTML limit exceeded', {
|