@etiquekit/etq 1.0.12 → 1.0.13
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/AGENTS.md +19 -26
- package/LICENSE +2 -3
- package/NOTICE +6 -4
- package/QuickStart.md +35 -47
- package/README.md +54 -56
- package/bin/etiquette +1 -5
- package/bin/etiquette-core +1 -5
- package/docs/ARCHITECTURE.md +5 -5
- package/docs/CODEX_CLIENT_COMPATIBILITY.md +1 -1
- package/docs/CONCEPTS.md +7 -23
- package/docs/CONCEPT_STATUS.md +66 -0
- package/docs/CORE_PROFILE.md +3 -5
- package/docs/LANE_PROVISIONING.md +7 -2
- package/docs/README.md +4 -4
- package/docs/RELEASE_SURFACE_AUDIT.md +7 -6
- package/docs/SEAT_DISCIPLINE.md +55 -91
- package/docs/SEAT_PROVISIONING.md +19 -22
- package/docs/TEAM_HANDOFF.md +21 -21
- package/docs/WORKTREE_QOL.md +7 -6
- package/docs/contracts/ledger-entry/README.md +11 -11
- package/docs/contracts/ledger-entry/ledger-entry.v0.2.md +3 -3
- package/docs/contracts/ledger-entry/ledger-entry.v0.md +8 -8
- package/package.json +11 -11
- package/packages/control/src/authority/lease.ts +261 -0
- package/packages/control/src/authority/node-delegation.ts +257 -0
- package/packages/control/src/authority/rig-conductor.ts +632 -0
- package/packages/control/src/cli/argv.ts +155 -0
- package/packages/control/src/cli/commands/console.ts +200 -0
- package/packages/control/src/cli/commands/dispatch-core.ts +89 -0
- package/packages/control/src/cli/commands/dispatch.ts +279 -0
- package/packages/control/src/cli/commands/harness.ts +89 -0
- package/packages/control/src/cli/commands/hook.ts +50 -0
- package/packages/control/src/cli/commands/ledger.ts +91 -0
- package/packages/control/src/cli/commands/local-workflow.ts +4686 -0
- package/packages/control/src/cli/commands/memory.ts +445 -0
- package/packages/control/src/cli/commands/release.ts +108 -0
- package/packages/control/src/cli/commands/rubric.ts +103 -0
- package/packages/control/src/cli/commands/seat.ts +179 -0
- package/packages/control/src/cli/commands/session.ts +127 -0
- package/packages/control/src/cli/commands/supervision.ts +246 -0
- package/packages/control/src/cli/commands/sync.ts +119 -0
- package/packages/control/src/cli/commands/workflow.ts +86 -0
- package/packages/control/src/cli/commands/workspace.ts +50 -0
- package/packages/control/src/cli/core-usage.ts +34 -0
- package/packages/control/src/cli/prompt.ts +67 -0
- package/packages/control/src/cli/supervision-deps.ts +44 -0
- package/packages/control/src/cli/usage.ts +241 -0
- package/packages/control/src/cli.ts +207 -0
- package/packages/control/src/core-cli.ts +50 -0
- package/packages/control/src/dispatch/decision.ts +202 -0
- package/packages/control/src/dispatch/projection.ts +293 -0
- package/packages/control/src/dispatch/record.ts +153 -0
- package/packages/control/src/engagement/project.ts +170 -0
- package/packages/control/src/fs.ts +19 -0
- package/packages/control/src/harness/pruning.ts +406 -0
- package/packages/control/src/hooks/dispatcher.ts +117 -0
- package/packages/control/src/hooks/outbox.ts +86 -0
- package/packages/control/src/hooks/sanitize.ts +6 -0
- package/packages/control/src/hooks/types.ts +34 -0
- package/packages/control/src/index.ts +384 -0
- package/packages/control/src/ledger/entry.ts +303 -0
- package/packages/control/src/ledger/indexer.ts +542 -0
- package/packages/control/src/memory/context.ts +149 -0
- package/packages/control/src/memory/drain-import.ts +207 -0
- package/packages/control/src/memory/indexer.ts +284 -0
- package/packages/control/src/memory/query.ts +75 -0
- package/packages/control/src/memory/sanitize.ts +50 -0
- package/packages/control/src/memory/sharded-drain-import.ts +212 -0
- package/packages/control/src/memory/status.ts +211 -0
- package/packages/control/src/memory/store-lifecycle.ts +509 -0
- package/packages/control/src/memory/store.ts +284 -0
- package/packages/control/src/memory/types.ts +146 -0
- package/packages/control/src/parity/surfaces.ts +748 -0
- package/packages/control/src/project.ts +141 -0
- package/packages/control/src/projection/local-ledger-view.ts +373 -0
- package/packages/control/src/projection/return-enforcement.ts +48 -0
- package/packages/control/src/projection/timeline-preview.ts +539 -0
- package/packages/control/src/projection/timeline.ts +708 -0
- package/packages/control/src/release/readiness.ts +831 -0
- package/packages/control/src/rubric/loader.ts +326 -0
- package/packages/control/src/rubric/promotion.ts +54 -0
- package/packages/control/src/rubric/runner.ts +159 -0
- package/packages/control/src/rubric/types.ts +158 -0
- package/packages/control/src/seat/owner-card.ts +388 -0
- package/packages/control/src/seat/readiness.ts +834 -0
- package/packages/control/src/session/runbook.ts +431 -0
- package/packages/control/src/shared/sanitize.ts +49 -0
- package/packages/control/src/supervision/action-classes.ts +192 -0
- package/packages/control/src/supervision/command-apply.ts +378 -0
- package/packages/control/src/supervision/errors.ts +14 -0
- package/packages/control/src/supervision/event-replay.ts +155 -0
- package/packages/control/src/supervision/events.ts +109 -0
- package/packages/control/src/supervision/index.ts +16 -0
- package/packages/control/src/supervision/manifest.ts +127 -0
- package/packages/control/src/supervision/paths.ts +49 -0
- package/packages/control/src/supervision/projection-adapter.ts +274 -0
- package/packages/control/src/supervision/projection.ts +75 -0
- package/packages/control/src/supervision/rebuild.ts +99 -0
- package/packages/control/src/supervision/session-open.ts +131 -0
- package/packages/control/src/supervision/session-read.ts +99 -0
- package/packages/control/src/supervision/sqlite-impl.ts +71 -0
- package/packages/control/src/supervision/sqlite.ts +121 -0
- package/packages/control/src/supervision/store-rows.ts +371 -0
- package/packages/control/src/supervision/turn-close.ts +154 -0
- package/packages/control/src/supervision/turn-open.ts +284 -0
- package/packages/control/src/sync/event-log-merge-driver.ts +263 -0
- package/packages/control/src/sync/join-plan.ts +375 -0
- package/packages/control/src/sync/outbox.ts +492 -0
- package/packages/control/src/workflow/evaluator.ts +140 -0
- package/packages/control/src/workflow/loader.ts +200 -0
- package/packages/control/src/workflow/types.ts +90 -0
- package/packages/control/src/workspace/authority.ts +499 -0
- package/packages/protocol/src/guards.ts +119 -0
- package/packages/protocol/src/huddle-board.ts +198 -0
- package/packages/protocol/src/huddle.ts +295 -0
- package/packages/protocol/src/incident.ts +251 -0
- package/packages/protocol/src/index.ts +8 -0
- package/packages/protocol/src/interfaces.ts +107 -0
- package/packages/protocol/src/packet-profile.ts +195 -0
- package/packages/protocol/src/state.ts +81 -0
- package/packages/protocol/src/types.ts +434 -0
- package/release/lineage.v0.json +14 -0
- package/scripts/release-candidate-verify.sh +175 -0
- package/scripts/release-checksum.sh +25 -0
- package/scripts/release-pack-canary.sh +97 -0
- package/scripts/release-scan.sh +249 -0
- package/scripts/release-sign.sh +34 -0
- package/templates/etiquette-vanilla-v0/README.md +2 -3
- package/templates/etiquette-vanilla-v0/source/control-seat/README.md +0 -6
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-doctor +0 -5
- package/templates/etiquette-vanilla-v0/validate-vanilla.sh +0 -5
- package/templates/seat-packs-v0/README.md +3 -2
- package/templates/seat-packs-v0/source/claude-code-seat/README.md +3 -29
- package/templates/seat-packs-v0/source/codex-seat/README.md +3 -17
- package/templates/seat-packs-v0/source/gemini-seat/README.md +3 -23
- package/templates/seat-packs-v0/source/ollama-seat/README.md +3 -3
- package/templates/seat-packs-v0/source/openrouter-seat/README.md +3 -3
- package/docs/AGENT_SPAWNING.md +0 -78
- package/docs/INTEGRATION_UI.md +0 -215
- package/docs/OPERATOR_PLAYBOOK.md +0 -77
- package/docs/TAG_ROUTE.md +0 -95
- package/lib/etiquette-core.js +0 -315
- package/lib/etiquette.js +0 -1160
- package/templates/DISPATCH_PREMISES.md +0 -39
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/access-assurance-check +0 -55
- package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/ACCESS-ASSURANCE.md +0 -39
- package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/workspace-secure-profile.v0.json +0 -53
- package/templates/hosted-receiver/README.md +0 -41
- package/templates/hosted-receiver/w1-github-webhook-receiver.mjs +0 -129
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { validateProjectManifest } from '../../protocol/src/index';
|
|
4
|
+
import type {
|
|
5
|
+
Dependency,
|
|
6
|
+
ProjectManifest,
|
|
7
|
+
Slice,
|
|
8
|
+
SliceBlueprint,
|
|
9
|
+
SliceTask,
|
|
10
|
+
SliceTaskBlueprint,
|
|
11
|
+
Surface,
|
|
12
|
+
} from '../../protocol/src/types';
|
|
13
|
+
import { now, readJson, sanitize } from './fs';
|
|
14
|
+
|
|
15
|
+
export async function loadProjectManifest(path?: string): Promise<ProjectManifest> {
|
|
16
|
+
const target = resolve(path ?? 'discipline.project.json');
|
|
17
|
+
const manifest = await readJson<ProjectManifest>(target);
|
|
18
|
+
const manifestDir = dirname(target);
|
|
19
|
+
const resolvePath = (value?: string): string | undefined => {
|
|
20
|
+
if (!value) return value;
|
|
21
|
+
return isAbsolute(value) ? value : resolve(manifestDir, value);
|
|
22
|
+
};
|
|
23
|
+
manifest.projectRoot = resolvePath(manifest.projectRoot)!;
|
|
24
|
+
manifest.handshakeStorePath = resolvePath(manifest.handshakeStorePath)!;
|
|
25
|
+
manifest.defaultWorktreeRoot = resolvePath(manifest.defaultWorktreeRoot)!;
|
|
26
|
+
if (manifest.logixRepoRoot) {
|
|
27
|
+
manifest.logixRepoRoot = resolvePath(manifest.logixRepoRoot);
|
|
28
|
+
}
|
|
29
|
+
for (const surface of Object.values(manifest.surfaces)) {
|
|
30
|
+
surface.repoRoots = surface.repoRoots.map((root) => resolvePath(root)!);
|
|
31
|
+
}
|
|
32
|
+
for (const slice of Object.values(manifest.slices)) {
|
|
33
|
+
if (slice.repoRoots) {
|
|
34
|
+
slice.repoRoots = slice.repoRoots.map((root) => resolvePath(root)!);
|
|
35
|
+
}
|
|
36
|
+
for (const task of Object.values(slice.tasks)) {
|
|
37
|
+
if (!task?.repoRoots) continue;
|
|
38
|
+
task.repoRoots = task.repoRoots.map((root) => resolvePath(root)!);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
validateProjectManifest(manifest);
|
|
42
|
+
return manifest;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function sliceStatePath(project: ProjectManifest, sliceId: string): string {
|
|
46
|
+
return join(project.handshakeStorePath, '.discipline', 'slices', `${sliceId}.json`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function loadSlice(project: ProjectManifest, sliceId: string): Promise<Slice | null> {
|
|
50
|
+
const path = sliceStatePath(project, sliceId);
|
|
51
|
+
if (!existsSync(path)) return null;
|
|
52
|
+
return readJson<Slice>(path);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveBlueprint(project: ProjectManifest, sliceId: string): SliceBlueprint {
|
|
56
|
+
const blueprint = project.slices[sliceId];
|
|
57
|
+
if (!blueprint) {
|
|
58
|
+
throw new Error(`unknown slice: ${sliceId}`);
|
|
59
|
+
}
|
|
60
|
+
return blueprint;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildTask(project: ProjectManifest, sliceId: string, scrutinyId: string, surface: Surface, task: SliceTaskBlueprint): SliceTask {
|
|
64
|
+
const defaults = project.surfaces[surface];
|
|
65
|
+
const taskId = `${sanitize(sliceId)}-${surface}`;
|
|
66
|
+
const ownedPaths = task.ownedPaths ?? defaults.ownedPaths;
|
|
67
|
+
const forbiddenPaths = task.forbiddenPaths ?? defaults.forbiddenPaths;
|
|
68
|
+
const checks = task.checks ?? defaults.checks;
|
|
69
|
+
const evidenceFiles = task.evidenceFiles ?? defaults.evidenceFiles;
|
|
70
|
+
const repoRoots = task.repoRoots ?? defaults.repoRoots;
|
|
71
|
+
const owner = task.owner ?? defaults.owner;
|
|
72
|
+
const workerRole = task.workerRole ?? defaults.workerRole;
|
|
73
|
+
return {
|
|
74
|
+
taskId,
|
|
75
|
+
surface,
|
|
76
|
+
title: task.title,
|
|
77
|
+
summary: task.summary,
|
|
78
|
+
scrutinyId,
|
|
79
|
+
state: 'incoming',
|
|
80
|
+
owner,
|
|
81
|
+
workerRole,
|
|
82
|
+
bounds: {
|
|
83
|
+
repoRoots,
|
|
84
|
+
ownedPaths,
|
|
85
|
+
forbiddenPaths,
|
|
86
|
+
checks,
|
|
87
|
+
evidenceFiles,
|
|
88
|
+
},
|
|
89
|
+
dependencies: task.dependencies ?? ([] as Dependency[]),
|
|
90
|
+
transitions: [
|
|
91
|
+
{
|
|
92
|
+
state: 'incoming',
|
|
93
|
+
actorId: owner,
|
|
94
|
+
at: now(),
|
|
95
|
+
note: 'task created from slice blueprint',
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildSlice(project: ProjectManifest, sliceId: string, surfaces: Surface[]): Slice {
|
|
102
|
+
const blueprint = resolveBlueprint(project, sliceId);
|
|
103
|
+
const tasks = surfaces.map((surface) => {
|
|
104
|
+
const taskBlueprint = blueprint.tasks[surface];
|
|
105
|
+
if (!taskBlueprint) {
|
|
106
|
+
throw new Error(`slice ${sliceId} missing task blueprint for surface ${surface}`);
|
|
107
|
+
}
|
|
108
|
+
return buildTask(project, sliceId, blueprint.scrutinyId, surface, taskBlueprint);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
schemaVersion: 'discipline-slice.v1',
|
|
113
|
+
sliceId,
|
|
114
|
+
projectId: project.projectId,
|
|
115
|
+
title: blueprint.title,
|
|
116
|
+
objective: blueprint.objective,
|
|
117
|
+
scrutinyId: blueprint.scrutinyId,
|
|
118
|
+
state: 'planned',
|
|
119
|
+
repoRoots: blueprint.repoRoots ?? [project.projectRoot],
|
|
120
|
+
handshakeStorePath: project.handshakeStorePath,
|
|
121
|
+
tasks,
|
|
122
|
+
transitions: [
|
|
123
|
+
{
|
|
124
|
+
state: 'planned',
|
|
125
|
+
actorId: 'etiquette',
|
|
126
|
+
at: now(),
|
|
127
|
+
note: 'slice initialized',
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
createdAt: now(),
|
|
131
|
+
updatedAt: now(),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function resolveWorktreePath(project: ProjectManifest, sliceId: string, task: SliceTask): string {
|
|
136
|
+
const template = project.surfaces[task.surface].worktreeTemplate;
|
|
137
|
+
return template
|
|
138
|
+
.replace('{root}', project.defaultWorktreeRoot)
|
|
139
|
+
.replace('{sliceId}', sanitize(sliceId))
|
|
140
|
+
.replace('{surface}', task.surface);
|
|
141
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
5
|
+
import { join, relative, resolve } from 'node:path';
|
|
6
|
+
|
|
7
|
+
type JsonRecord = Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
export interface LocalEvent extends JsonRecord {
|
|
10
|
+
seq: number;
|
|
11
|
+
schema: 'local-event.v0';
|
|
12
|
+
event_ref: string;
|
|
13
|
+
created_at: string;
|
|
14
|
+
type: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type LocalLedgerViewKind = 'all' | 'events' | 'packets';
|
|
18
|
+
|
|
19
|
+
export interface LocalLedgerViewOptions {
|
|
20
|
+
since?: string | number | null;
|
|
21
|
+
until?: string | number | null;
|
|
22
|
+
lane?: string | null;
|
|
23
|
+
seat?: string | null;
|
|
24
|
+
from?: string | null;
|
|
25
|
+
to?: string | null;
|
|
26
|
+
role?: string | null;
|
|
27
|
+
action?: string | null;
|
|
28
|
+
match?: string | null;
|
|
29
|
+
kind?: LocalLedgerViewKind;
|
|
30
|
+
limit?: number;
|
|
31
|
+
computedAt?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface LocalLedgerViewRow {
|
|
35
|
+
kind: 'event' | 'packet';
|
|
36
|
+
seq: number | null;
|
|
37
|
+
event_ref: string | null;
|
|
38
|
+
created_at: string | null;
|
|
39
|
+
type: string | null;
|
|
40
|
+
role: string | null;
|
|
41
|
+
lane: string | null;
|
|
42
|
+
from: string | null;
|
|
43
|
+
to: string | null;
|
|
44
|
+
next_owner: string | null;
|
|
45
|
+
packet_ref: string | null;
|
|
46
|
+
requires_ack: boolean | null;
|
|
47
|
+
verdict: string | null;
|
|
48
|
+
status_effect: string | null;
|
|
49
|
+
topic_hints: string[];
|
|
50
|
+
in_reply_to: string | null;
|
|
51
|
+
parent_event_ref: string | null;
|
|
52
|
+
parent_event_seq: number | null;
|
|
53
|
+
mode: string | null;
|
|
54
|
+
packet_tier: string | null;
|
|
55
|
+
text: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface LocalLedgerView {
|
|
59
|
+
schema: 'local-ledger-view.v0';
|
|
60
|
+
watermark: {
|
|
61
|
+
latest_event_ref: string;
|
|
62
|
+
latest_seq: number;
|
|
63
|
+
commit_sha: string;
|
|
64
|
+
event_log_hash: string;
|
|
65
|
+
computed_at: string;
|
|
66
|
+
};
|
|
67
|
+
filters: JsonRecord;
|
|
68
|
+
boundary: string;
|
|
69
|
+
events_matched: number;
|
|
70
|
+
packets_matched: number;
|
|
71
|
+
events: LocalLedgerViewRow[];
|
|
72
|
+
packets: LocalLedgerViewRow[];
|
|
73
|
+
authority_boundary: {
|
|
74
|
+
stdout_only: true;
|
|
75
|
+
rebuildable_projection: true;
|
|
76
|
+
can_authorize: false;
|
|
77
|
+
can_assign: false;
|
|
78
|
+
can_close: false;
|
|
79
|
+
can_merge: false;
|
|
80
|
+
can_mutate_bus: false;
|
|
81
|
+
can_write_global_ledger: false;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function eventPath(root: string): string {
|
|
86
|
+
return join(root, '.etiquette', 'events', 'local.ndjson');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function relativeRef(root: string, path: string): string {
|
|
90
|
+
const rel = relative(root, resolve(path));
|
|
91
|
+
if (rel.length === 0 || rel.startsWith('..') || rel.includes('\\')) {
|
|
92
|
+
throw new Error(`path must stay inside project root: ${path}`);
|
|
93
|
+
}
|
|
94
|
+
return rel;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function asRecord(value: unknown, label: string): JsonRecord {
|
|
98
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
99
|
+
throw new Error(`${label} must be an object`);
|
|
100
|
+
}
|
|
101
|
+
return value as JsonRecord;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function stringField(record: JsonRecord, field: string): string | undefined {
|
|
105
|
+
const value = record[field];
|
|
106
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function boolField(record: JsonRecord, field: string): boolean | undefined {
|
|
110
|
+
const value = record[field];
|
|
111
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function numberField(record: JsonRecord, field: string): number | undefined {
|
|
115
|
+
const value = record[field];
|
|
116
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function stringListField(record: JsonRecord, field: string): string[] {
|
|
120
|
+
const value = record[field];
|
|
121
|
+
if (!Array.isArray(value)) return [];
|
|
122
|
+
return value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0).map((item) => item.trim());
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function readTextIfExists(path: string): Promise<string | null> {
|
|
126
|
+
try {
|
|
127
|
+
return await readFile(path, 'utf-8');
|
|
128
|
+
} catch (err) {
|
|
129
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function listJsonFiles(dir: string): Promise<string[]> {
|
|
135
|
+
if (!existsSync(dir)) return [];
|
|
136
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
137
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith('.json')).map((entry) => join(dir, entry.name));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function readJsonIfExists<T>(path: string, fallback: T): Promise<T> {
|
|
141
|
+
const raw = await readTextIfExists(path);
|
|
142
|
+
return raw ? JSON.parse(raw) as T : fallback;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function readEventsIfInitialized(root: string): Promise<LocalEvent[]> {
|
|
146
|
+
const raw = await readTextIfExists(eventPath(root));
|
|
147
|
+
if (!raw) return [];
|
|
148
|
+
return raw
|
|
149
|
+
.split('\n')
|
|
150
|
+
.map((line) => line.trim())
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
.map((line) => JSON.parse(line) as LocalEvent);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function parseBusSeqBound(raw: string | number | null | undefined, key: string): number | null {
|
|
156
|
+
if (raw === undefined || raw === null) return null;
|
|
157
|
+
if (typeof raw === 'number') {
|
|
158
|
+
if (!Number.isFinite(raw) || raw < 0) throw new Error(`--${key} must be a non-negative event sequence`);
|
|
159
|
+
return raw;
|
|
160
|
+
}
|
|
161
|
+
const normalized = raw.startsWith('local://event/') ? raw.slice('local://event/'.length) : raw;
|
|
162
|
+
const n = Number.parseInt(normalized, 10);
|
|
163
|
+
if (!Number.isFinite(n) || n < 0) throw new Error(`--${key} must be a non-negative event sequence`);
|
|
164
|
+
return n;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function eventLogHash(raw: string | null): string {
|
|
168
|
+
return `sha256:${createHash('sha256').update(raw ?? '').digest('hex')}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function currentGitCommit(root: string): string {
|
|
172
|
+
const result = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf-8' });
|
|
173
|
+
if (result.status !== 0) return 'unavailable';
|
|
174
|
+
return result.stdout.trim() || 'unavailable';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function rowText(row: LocalLedgerViewRow): string {
|
|
178
|
+
const parts = [
|
|
179
|
+
row.event_ref ?? row.packet_ref ?? row.kind,
|
|
180
|
+
row.type,
|
|
181
|
+
row.role ? `role=${row.role}` : null,
|
|
182
|
+
row.lane ? `lane=${row.lane}` : null,
|
|
183
|
+
row.from ? `from=${row.from}` : null,
|
|
184
|
+
row.to ? `to=${row.to}` : null,
|
|
185
|
+
row.next_owner ? `next_owner=${row.next_owner}` : null,
|
|
186
|
+
row.requires_ack === true ? 'requires_ack=true' : null,
|
|
187
|
+
row.packet_ref ? `packet_ref=${row.packet_ref}` : null,
|
|
188
|
+
row.verdict ? `verdict=${row.verdict}` : null,
|
|
189
|
+
row.status_effect && row.status_effect !== 'none' ? `status_effect=${row.status_effect}` : null,
|
|
190
|
+
row.topic_hints.length > 0 ? `topic_hints=${row.topic_hints.join(',')}` : null,
|
|
191
|
+
row.in_reply_to ? `in_reply_to=${row.in_reply_to}` : null,
|
|
192
|
+
].filter((value): value is string => value !== null && value !== undefined && value.length > 0);
|
|
193
|
+
return parts.join(' ');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function eventToBusViewRow(event: LocalEvent): LocalLedgerViewRow {
|
|
197
|
+
const actor = typeof event.actor === 'string' ? event.actor : null;
|
|
198
|
+
const row: LocalLedgerViewRow = {
|
|
199
|
+
kind: 'event',
|
|
200
|
+
seq: event.seq,
|
|
201
|
+
event_ref: event.event_ref,
|
|
202
|
+
created_at: event.created_at,
|
|
203
|
+
type: event.type,
|
|
204
|
+
role: typeof event.role === 'string' ? event.role : null,
|
|
205
|
+
lane: typeof event.lane === 'string' ? event.lane : null,
|
|
206
|
+
from: typeof event.from === 'string' ? event.from : actor,
|
|
207
|
+
to: typeof event.to === 'string' ? event.to : null,
|
|
208
|
+
next_owner: typeof event.next_owner === 'string' ? event.next_owner : null,
|
|
209
|
+
packet_ref: typeof event.packet_ref === 'string' ? event.packet_ref : null,
|
|
210
|
+
requires_ack: typeof event.requires_ack === 'boolean' ? event.requires_ack : null,
|
|
211
|
+
verdict: typeof event.verdict === 'string' ? event.verdict : null,
|
|
212
|
+
status_effect: typeof event.status_effect === 'string' ? event.status_effect : null,
|
|
213
|
+
topic_hints: stringListField(event, 'topic_hints'),
|
|
214
|
+
in_reply_to: typeof event.in_reply_to === 'string' ? event.in_reply_to : null,
|
|
215
|
+
parent_event_ref: typeof event.parent_event_ref === 'string' ? event.parent_event_ref : null,
|
|
216
|
+
parent_event_seq: numberField(event, 'parent_event_seq') ?? null,
|
|
217
|
+
mode: typeof event.mode === 'string' ? event.mode : null,
|
|
218
|
+
packet_tier: typeof event.packet_tier === 'string' ? event.packet_tier : null,
|
|
219
|
+
text: '',
|
|
220
|
+
};
|
|
221
|
+
return { ...row, text: rowText(row) };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function packetToBusViewRow(packet: JsonRecord, packetRef: string, eventSeq: number | null): LocalLedgerViewRow {
|
|
225
|
+
const row: LocalLedgerViewRow = {
|
|
226
|
+
kind: 'packet',
|
|
227
|
+
seq: eventSeq,
|
|
228
|
+
event_ref: null,
|
|
229
|
+
created_at: stringField(packet, 'created_at') ?? null,
|
|
230
|
+
type: stringField(packet, 'action') ?? stringField(packet, 'type') ?? null,
|
|
231
|
+
role: stringField(packet, 'role') ?? null,
|
|
232
|
+
lane: stringField(packet, 'lane') ?? null,
|
|
233
|
+
from: stringField(packet, 'from') ?? null,
|
|
234
|
+
to: stringField(packet, 'to') ?? null,
|
|
235
|
+
next_owner: stringField(packet, 'next_owner') ?? null,
|
|
236
|
+
packet_ref: packetRef,
|
|
237
|
+
requires_ack: boolField(packet, 'requires_ack') ?? null,
|
|
238
|
+
verdict: stringField(packet, 'verdict') ?? null,
|
|
239
|
+
status_effect: stringField(packet, 'status_effect') ?? null,
|
|
240
|
+
topic_hints: stringListField(packet, 'topic_hints'),
|
|
241
|
+
in_reply_to: stringField(packet, 'in_reply_to') ?? null,
|
|
242
|
+
parent_event_ref: stringField(packet, 'parent_event_ref') ?? null,
|
|
243
|
+
parent_event_seq: numberField(packet, 'parent_event_seq') ?? null,
|
|
244
|
+
mode: stringField(packet, 'mode') ?? null,
|
|
245
|
+
packet_tier: stringField(packet, 'packet_tier') ?? null,
|
|
246
|
+
text: '',
|
|
247
|
+
};
|
|
248
|
+
return { ...row, text: rowText(row) };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function readPacketRows(root: string, events: LocalEvent[]): Promise<LocalLedgerViewRow[]> {
|
|
252
|
+
const packetDir = join(root, 'docs', 'work', 'packets');
|
|
253
|
+
if (!existsSync(packetDir)) return [];
|
|
254
|
+
const files = await listJsonFiles(packetDir);
|
|
255
|
+
const eventSeqByPacketRef = new Map(
|
|
256
|
+
events
|
|
257
|
+
.filter((event) => typeof event.packet_ref === 'string')
|
|
258
|
+
.map((event) => [String(event.packet_ref), event.seq] as const),
|
|
259
|
+
);
|
|
260
|
+
const rows: LocalLedgerViewRow[] = [];
|
|
261
|
+
for (const path of files.sort()) {
|
|
262
|
+
const packetRef = relativeRef(root, path);
|
|
263
|
+
const packet = asRecord(await readJsonIfExists<JsonRecord>(path, {}), 'local packet');
|
|
264
|
+
rows.push(packetToBusViewRow(packet, packetRef, eventSeqByPacketRef.get(packetRef) ?? null));
|
|
265
|
+
}
|
|
266
|
+
return rows;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function rowMatchesBusView(row: LocalLedgerViewRow, options: LocalLedgerViewOptions): boolean {
|
|
270
|
+
const since = parseBusSeqBound(options.since, 'since');
|
|
271
|
+
const until = parseBusSeqBound(options.until, 'until');
|
|
272
|
+
if (since !== null && (row.seq === null || row.seq < since)) return false;
|
|
273
|
+
if (until !== null && (row.seq === null || row.seq > until)) return false;
|
|
274
|
+
|
|
275
|
+
if (options.lane && row.lane !== options.lane) return false;
|
|
276
|
+
if (options.role && row.role !== options.role) return false;
|
|
277
|
+
if (options.action && row.type !== options.action) return false;
|
|
278
|
+
if (options.from && row.from !== options.from) return false;
|
|
279
|
+
if (options.to && row.to !== options.to && row.next_owner !== options.to) return false;
|
|
280
|
+
if (options.seat && row.from !== options.seat && row.to !== options.seat && row.next_owner !== options.seat && !row.text.includes(options.seat)) return false;
|
|
281
|
+
if (options.match && !row.text.toLowerCase().includes(options.match.toLowerCase())) return false;
|
|
282
|
+
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function buildLocalLedgerView(root: string, options: LocalLedgerViewOptions = {}): Promise<LocalLedgerView> {
|
|
287
|
+
const rawEventLog = await readTextIfExists(eventPath(root));
|
|
288
|
+
const events = await readEventsIfInitialized(root);
|
|
289
|
+
const packetRows = await readPacketRows(root, events);
|
|
290
|
+
const limit = options.limit ?? 20;
|
|
291
|
+
const kind = options.kind ?? 'all';
|
|
292
|
+
if (limit < 0) throw new Error('--limit must be a non-negative integer');
|
|
293
|
+
const eventRows = events.map(eventToBusViewRow).filter((row) => rowMatchesBusView(row, options));
|
|
294
|
+
const filteredPacketRows = packetRows.filter((row) => rowMatchesBusView(row, options));
|
|
295
|
+
const latestSeq = events.reduce((max, event) => Math.max(max, event.seq), 0);
|
|
296
|
+
const latestEventRef = latestSeq > 0 ? `local://event/${latestSeq}` : 'local://event/0';
|
|
297
|
+
const takeLast = <T>(rows: T[]): T[] => (limit === 0 ? [] : rows.slice(-limit));
|
|
298
|
+
return {
|
|
299
|
+
schema: 'local-ledger-view.v0',
|
|
300
|
+
watermark: {
|
|
301
|
+
latest_event_ref: latestEventRef,
|
|
302
|
+
latest_seq: latestSeq,
|
|
303
|
+
commit_sha: currentGitCommit(root),
|
|
304
|
+
event_log_hash: eventLogHash(rawEventLog),
|
|
305
|
+
computed_at: options.computedAt ?? new Date().toISOString(),
|
|
306
|
+
},
|
|
307
|
+
filters: {
|
|
308
|
+
since: options.since ?? null,
|
|
309
|
+
until: options.until ?? null,
|
|
310
|
+
lane: options.lane ?? null,
|
|
311
|
+
seat: options.seat ?? null,
|
|
312
|
+
from: options.from ?? null,
|
|
313
|
+
to: options.to ?? null,
|
|
314
|
+
role: options.role ?? null,
|
|
315
|
+
action: options.action ?? null,
|
|
316
|
+
match: options.match ?? null,
|
|
317
|
+
kind,
|
|
318
|
+
limit,
|
|
319
|
+
},
|
|
320
|
+
boundary: 'authority-false stdout-only rebuildable local projection; cannot authorize, assign, close, merge, mutate local events, write global ledger, or read external buses',
|
|
321
|
+
events_matched: eventRows.length,
|
|
322
|
+
packets_matched: filteredPacketRows.length,
|
|
323
|
+
events: kind === 'packets' ? [] : takeLast(eventRows),
|
|
324
|
+
packets: kind === 'events' ? [] : takeLast(filteredPacketRows),
|
|
325
|
+
authority_boundary: {
|
|
326
|
+
stdout_only: true,
|
|
327
|
+
rebuildable_projection: true,
|
|
328
|
+
can_authorize: false,
|
|
329
|
+
can_assign: false,
|
|
330
|
+
can_close: false,
|
|
331
|
+
can_merge: false,
|
|
332
|
+
can_mutate_bus: false,
|
|
333
|
+
can_write_global_ledger: false,
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function renderLocalLedgerViewText(view: LocalLedgerView): string {
|
|
339
|
+
return [
|
|
340
|
+
`schema: ${view.schema}`,
|
|
341
|
+
`watermark: ${view.watermark.latest_event_ref} commit_sha=${view.watermark.commit_sha}`,
|
|
342
|
+
`event_log_hash: ${view.watermark.event_log_hash}`,
|
|
343
|
+
`computed_at: ${view.watermark.computed_at}`,
|
|
344
|
+
`filters: ${Object.entries(view.filters).filter(([, value]) => value !== null).map(([key, value]) => `${key}=${String(value)}`).join(', ') || 'none'}`,
|
|
345
|
+
`boundary: ${view.boundary}`,
|
|
346
|
+
`events_matched: ${view.events_matched} showing: ${view.events.length}`,
|
|
347
|
+
...view.events.map((row) => ` - ${row.text}`),
|
|
348
|
+
`packets_matched: ${view.packets_matched} showing: ${view.packets.length}`,
|
|
349
|
+
...view.packets.map((row) => ` - ${row.text}`),
|
|
350
|
+
].join('\n');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function renderLocalLedgerViewMarkdown(view: LocalLedgerView): string {
|
|
354
|
+
return [
|
|
355
|
+
'# Local Etiquette Bus View',
|
|
356
|
+
'',
|
|
357
|
+
`- Schema: \`${view.schema}\``,
|
|
358
|
+
`- Watermark: \`${view.watermark.latest_event_ref}\``,
|
|
359
|
+
`- Commit: \`${view.watermark.commit_sha}\``,
|
|
360
|
+
`- Event log hash: \`${view.watermark.event_log_hash}\``,
|
|
361
|
+
`- Computed at: \`${view.watermark.computed_at}\``,
|
|
362
|
+
`- Boundary: ${view.boundary}`,
|
|
363
|
+
'',
|
|
364
|
+
`## Events (${view.events_matched} matched, ${view.events.length} shown)`,
|
|
365
|
+
'',
|
|
366
|
+
...(view.events.length === 0 ? ['_No events._'] : view.events.map((row) => `- ${row.text}`)),
|
|
367
|
+
'',
|
|
368
|
+
`## Packets (${view.packets_matched} matched, ${view.packets.length} shown)`,
|
|
369
|
+
'',
|
|
370
|
+
...(view.packets.length === 0 ? ['_No packets._'] : view.packets.map((row) => `- ${row.text}`)),
|
|
371
|
+
'',
|
|
372
|
+
].join('\n');
|
|
373
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { LocalLedgerView, LocalLedgerViewRow } from './local-ledger-view';
|
|
2
|
+
|
|
3
|
+
export type ReturnRole = 'implementation-return' | 'review-return';
|
|
4
|
+
export type ReturnCheckVerdict = 'pass' | 'missing_return' | 'unposted_return';
|
|
5
|
+
|
|
6
|
+
export interface ReturnDetection {
|
|
7
|
+
task: string;
|
|
8
|
+
seat: string;
|
|
9
|
+
expected_roles: ReturnRole[];
|
|
10
|
+
verdict: ReturnCheckVerdict;
|
|
11
|
+
matched_return_packets: LocalLedgerViewRow[];
|
|
12
|
+
posted_return_packets: LocalLedgerViewRow[];
|
|
13
|
+
latest_event_ref: string;
|
|
14
|
+
event_log_hash: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function detectReturnPackets(
|
|
18
|
+
projection: Pick<LocalLedgerView, 'packets' | 'watermark'>,
|
|
19
|
+
options: {
|
|
20
|
+
task: string;
|
|
21
|
+
seat: string;
|
|
22
|
+
expectedRoles: ReturnRole[];
|
|
23
|
+
},
|
|
24
|
+
): ReturnDetection {
|
|
25
|
+
const matchedReturnPackets = projection.packets.filter((row) => (
|
|
26
|
+
row.kind === 'packet' &&
|
|
27
|
+
row.lane === options.task &&
|
|
28
|
+
row.from === options.seat &&
|
|
29
|
+
options.expectedRoles.includes(row.role as ReturnRole)
|
|
30
|
+
));
|
|
31
|
+
const postedReturnPackets = matchedReturnPackets.filter((row) => row.seq !== null);
|
|
32
|
+
const verdict: ReturnCheckVerdict = postedReturnPackets.length > 0
|
|
33
|
+
? 'pass'
|
|
34
|
+
: matchedReturnPackets.length > 0
|
|
35
|
+
? 'unposted_return'
|
|
36
|
+
: 'missing_return';
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
task: options.task,
|
|
40
|
+
seat: options.seat,
|
|
41
|
+
expected_roles: options.expectedRoles,
|
|
42
|
+
verdict,
|
|
43
|
+
matched_return_packets: matchedReturnPackets,
|
|
44
|
+
posted_return_packets: postedReturnPackets,
|
|
45
|
+
latest_event_ref: projection.watermark.latest_event_ref,
|
|
46
|
+
event_log_hash: projection.watermark.event_log_hash,
|
|
47
|
+
};
|
|
48
|
+
}
|