sphica 0.6.2 → 0.6.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +3 -0
- package/db/migrations/0003.sql +193 -0
- package/db/schema.sql +18 -5
- package/dist/capture.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/deliver.js +1 -1
- package/dist/mcp-record.js +56 -12
- package/dist/mcp.js +1112 -545
- package/package.json +1 -1
- package/skills/rules/SKILL.md +52 -0
- package/skills/rules/agents/openai.yaml +2 -0
- package/skills/trace/SKILL.md +4 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "sphica",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.4",
|
|
5
5
|
"description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "iroha924",
|
package/README.md
CHANGED
|
@@ -25,6 +25,9 @@ The database is a single SQLite file on your machine.
|
|
|
25
25
|
- **Forget what should not have been kept.** `/sphica:forget` removes the messages, pull request items, or file excerpts you pick, with their search entries and the bytes left in the database file, after you confirm in a dialog (if another session is reading the database, it asks you to run it again to finish clearing the bytes). Records that cited them are judged again and leave active when nothing else supports them; a record's own text stays as it was.
|
|
26
26
|
- **Shown when it matters.** At session start, the current work; before the agent reads or edits a file, or runs a shell command that names it, the decisions tied to that file; when your prompt names a recorded option or code symbol, that record. Works in both Claude Code and Codex.
|
|
27
27
|
- **Search in Japanese and English.** Records are made with search words in both languages, so a question in either language is more likely to find them.
|
|
28
|
+
- **Find what you asked before.** Ask the agent whether you asked something like this before: `search` with `asked: true` shows your earlier messages in other sessions, the records that quote them (with what replaced them), and says "no recorded decision" when none was recorded, including a matter you raised in several sessions.
|
|
29
|
+
- **See what is live, and what needs a look.** Ask for the overview: `view: "live"` lists every active decision and constraint by the directory it applies to; `view: "look"` lists records whose file is gone or whose symbol is not found, conditions you said would bring a rejected option back, and lines in your instruction files whose record was replaced. Nothing is expired or changed on its own.
|
|
30
|
+
- **Rule text from records.** `/sphica:rules` drafts lines for CLAUDE.md, AGENTS.md, or `.claude/rules` from the constraints and decisions you pick, each marked with its record key so the overview flags it once the record changes. It never edits the file.
|
|
28
31
|
- **Reviews check past decisions.** `/sphica:review` runs a reviewer per focus (correctness, security, written conventions, and past decisions by default; redundancy with `full`), and checks the diff against the records it touches.
|
|
29
32
|
|
|
30
33
|
Records are never rewritten: a correction is a new record that supersedes the old one, and the history stays.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
-- Revision 2 → 3 of generation 2: a rejected option can carry a reconsider condition the owner stated, quoted as `reconsiders` evidence (issue #193).
|
|
2
|
+
-- `sphica init` runs this in one transaction with foreign keys off (set outside the transaction), then checks foreign_key_check before
|
|
3
|
+
-- committing. Every statement matches db/schema.sql at revision 3; server/test/migrate.test.ts compares a migrated database with a fresh one.
|
|
4
|
+
|
|
5
|
+
-- Rebuild unit_option and unit_evidence (https://www.sqlite.org/lang_altertable.html#otheralter). What names them from elsewhere is dropped
|
|
6
|
+
-- first, or the renames fail on it: the state rules, the search view, and the triggers that read the view. All are created again after.
|
|
7
|
+
drop trigger unit_state_rules;
|
|
8
|
+
drop trigger unit_fts_ai;
|
|
9
|
+
drop trigger unit_fts_anchor_i;
|
|
10
|
+
drop trigger unit_fts_anchor_u;
|
|
11
|
+
drop trigger unit_fts_anchor_d;
|
|
12
|
+
drop trigger unit_fts_alias_i;
|
|
13
|
+
drop trigger unit_fts_alias_d;
|
|
14
|
+
drop view unit_search_text;
|
|
15
|
+
|
|
16
|
+
create table unit_option_new (
|
|
17
|
+
id integer primary key autoincrement not null,
|
|
18
|
+
unit_id integer not null references unit (id) on delete cascade,
|
|
19
|
+
position integer not null check (position > 0),
|
|
20
|
+
text text not null check (text <> ''),
|
|
21
|
+
outcome text not null check (outcome in ('chosen', 'rejected', 'deferred', 'proposed')),
|
|
22
|
+
why text,
|
|
23
|
+
-- For a rejected option, what the owner said would make it worth reconsidering; its words are a `reconsiders` evidence row on the option
|
|
24
|
+
reconsider_when text check (reconsider_when <> ''),
|
|
25
|
+
unique (unit_id, position),
|
|
26
|
+
check (reconsider_when is null or outcome = 'rejected'),
|
|
27
|
+
unique (unit_id, id)
|
|
28
|
+
) strict;
|
|
29
|
+
insert into unit_option_new (id, unit_id, position, text, outcome, why)
|
|
30
|
+
select id, unit_id, position, text, outcome, why from unit_option;
|
|
31
|
+
-- AUTOINCREMENT never reuses an id: carry the counter over
|
|
32
|
+
delete from sqlite_sequence where name = 'unit_option_new';
|
|
33
|
+
insert into sqlite_sequence (name, seq) select 'unit_option_new', seq from sqlite_sequence where name = 'unit_option';
|
|
34
|
+
create table unit_evidence_new (
|
|
35
|
+
id integer primary key autoincrement not null,
|
|
36
|
+
unit_id integer not null references unit (id) on delete cascade,
|
|
37
|
+
option_id integer,
|
|
38
|
+
source_id integer not null references source (id) on delete cascade,
|
|
39
|
+
span_start integer not null check (span_start >= 0),
|
|
40
|
+
span_end integer not null check (span_end > span_start),
|
|
41
|
+
role text not null check (role in ('states', 'proposes', 'rejects', 'explains', 'implements', 'reconsiders')),
|
|
42
|
+
-- A third party the owner reported ("X said ..."): hearsay by the owner, never X's own statement
|
|
43
|
+
reported_speaker text,
|
|
44
|
+
run_id integer not null references extraction_run (id),
|
|
45
|
+
added_at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', added_at) is added_at),
|
|
46
|
+
retracted_at text check (strftime('%Y-%m-%dT%H:%M:%fZ', retracted_at) is retracted_at),
|
|
47
|
+
retraction_reason text,
|
|
48
|
+
retraction_source_id integer references source (id),
|
|
49
|
+
retraction_span_start integer,
|
|
50
|
+
retraction_span_end integer,
|
|
51
|
+
foreign key (unit_id, option_id) references unit_option (unit_id, id) on delete cascade,
|
|
52
|
+
check ((retracted_at is null) = (retraction_reason is null)),
|
|
53
|
+
check ((retracted_at is null) = (retraction_source_id is null)),
|
|
54
|
+
check ((retraction_source_id is null) = (retraction_span_start is null)),
|
|
55
|
+
check ((retraction_source_id is null) = (retraction_span_end is null)),
|
|
56
|
+
check (retraction_span_end is null or retraction_span_end > retraction_span_start)
|
|
57
|
+
) strict;
|
|
58
|
+
insert into unit_evidence_new (id, unit_id, option_id, source_id, span_start, span_end, role, reported_speaker, run_id, added_at, retracted_at, retraction_reason, retraction_source_id, retraction_span_start, retraction_span_end)
|
|
59
|
+
select id, unit_id, option_id, source_id, span_start, span_end, role, reported_speaker, run_id, added_at, retracted_at, retraction_reason, retraction_source_id, retraction_span_start, retraction_span_end from unit_evidence;
|
|
60
|
+
-- AUTOINCREMENT never reuses an id: carry the counter over
|
|
61
|
+
delete from sqlite_sequence where name = 'unit_evidence_new';
|
|
62
|
+
insert into sqlite_sequence (name, seq) select 'unit_evidence_new', seq from sqlite_sequence where name = 'unit_evidence';
|
|
63
|
+
drop table unit_evidence;
|
|
64
|
+
drop table unit_option;
|
|
65
|
+
alter table unit_option_new rename to unit_option;
|
|
66
|
+
alter table unit_evidence_new rename to unit_evidence;
|
|
67
|
+
|
|
68
|
+
create trigger unit_option_sealed before insert on unit_option
|
|
69
|
+
when exists (select 1 from unit_state where unit_id = new.unit_id) or exists (select 1 from unit_alias where unit_id = new.unit_id) begin
|
|
70
|
+
select raise(abort, 'options are written with the unit, before its first state; record a successor instead');
|
|
71
|
+
end;
|
|
72
|
+
create trigger unit_option_frozen before update on unit_option begin
|
|
73
|
+
select raise(abort, 'options are never rewritten; record a successor');
|
|
74
|
+
end;
|
|
75
|
+
create trigger unit_option_no_delete before delete on unit_option
|
|
76
|
+
when exists (select 1 from unit where id = old.unit_id) begin
|
|
77
|
+
select raise(abort, 'options are never removed on their own');
|
|
78
|
+
end;
|
|
79
|
+
create unique index unit_evidence_unit_once on unit_evidence (unit_id, source_id, span_start, span_end, role) where option_id is null;
|
|
80
|
+
create unique index unit_evidence_option_once on unit_evidence (option_id, source_id, span_start, span_end, role) where option_id is not null;
|
|
81
|
+
create index unit_evidence_source on unit_evidence (source_id);
|
|
82
|
+
create trigger unit_evidence_check before insert on unit_evidence begin
|
|
83
|
+
select raise(abort, 'evidence and unit belong to different projects')
|
|
84
|
+
where (select project_id from unit where id = new.unit_id) is not (select project_id from source where id = new.source_id)
|
|
85
|
+
or (select project_id from unit where id = new.unit_id) is not (select project_id from extraction_run where id = new.run_id);
|
|
86
|
+
select raise(abort, 'evidence span is outside the source text')
|
|
87
|
+
where new.span_end > (select length(cast(text as blob)) from source where id = new.source_id);
|
|
88
|
+
select raise(abort, 'a reported speaker is the owner reporting someone else, so it must cite an owner session message')
|
|
89
|
+
where new.reported_speaker is not null and (trim(new.reported_speaker) = '' or not exists (select 1 from source
|
|
90
|
+
where id = new.source_id and kind = 'session_message' and author_kind = 'owner'));
|
|
91
|
+
select raise(abort, 'reconsiders quotes the owner on a rejected option that has a reconsider condition')
|
|
92
|
+
where new.role = 'reconsiders' and (new.option_id is null
|
|
93
|
+
or not exists (select 1 from unit_option where id = new.option_id and reconsider_when is not null)
|
|
94
|
+
or not exists (select 1 from source where id = new.source_id and author_kind = 'owner'));
|
|
95
|
+
end;
|
|
96
|
+
create trigger unit_evidence_retract before update on unit_evidence begin
|
|
97
|
+
select raise(abort, 'evidence is only ever retracted, once')
|
|
98
|
+
where old.retracted_at is not null or new.unit_id is not old.unit_id or new.option_id is not old.option_id
|
|
99
|
+
or new.source_id is not old.source_id or new.span_start is not old.span_start or new.span_end is not old.span_end
|
|
100
|
+
or new.role is not old.role or new.reported_speaker is not old.reported_speaker or new.run_id is not old.run_id
|
|
101
|
+
or new.added_at is not old.added_at or new.retracted_at is null;
|
|
102
|
+
select raise(abort, 'the retraction must cite an owner span of the same project')
|
|
103
|
+
where not exists (select 1 from source s where s.id = new.retraction_source_id and s.author_kind = 'owner'
|
|
104
|
+
and s.project_id = (select project_id from unit where id = new.unit_id)
|
|
105
|
+
and new.retraction_span_end <= length(cast(s.text as blob)));
|
|
106
|
+
end;
|
|
107
|
+
create trigger unit_evidence_no_delete before delete on unit_evidence
|
|
108
|
+
when exists (select 1 from unit where id = old.unit_id) and exists (select 1 from source where id = old.source_id)
|
|
109
|
+
and not (old.retracted_at is not null and exists (select 1 from source_forgotten where source_id = old.retraction_source_id)) begin
|
|
110
|
+
select raise(abort, 'evidence is retracted, never deleted');
|
|
111
|
+
end;
|
|
112
|
+
create trigger unit_evidence_retract_support after update of retracted_at on unit_evidence
|
|
113
|
+
when exists (select 1 from unit where id = new.unit_id and lifecycle = 'active')
|
|
114
|
+
and not exists (select 1 from unit_evidence where unit_id = new.unit_id and retracted_at is null) begin
|
|
115
|
+
select raise(abort, 'move the unit back to candidate before retracting its last evidence');
|
|
116
|
+
end;
|
|
117
|
+
create trigger unit_rev_evidence_i after insert on unit_evidence begin update unit set revision = revision + 1 where id = new.unit_id; end;
|
|
118
|
+
create trigger unit_rev_evidence_u after update on unit_evidence begin update unit set revision = revision + 1 where id = new.unit_id; end;
|
|
119
|
+
create trigger unit_rev_evidence_d after delete on unit_evidence when exists (select 1 from unit where id = old.unit_id) begin
|
|
120
|
+
update unit set revision = revision + 1 where id = old.unit_id;
|
|
121
|
+
end;
|
|
122
|
+
create trigger unit_state_rules before insert on unit_state begin
|
|
123
|
+
select raise(abort, 'from_state must be the current lifecycle')
|
|
124
|
+
where new.from_state is not (select lifecycle from unit where id = new.unit_id)
|
|
125
|
+
and exists (select 1 from unit_state where unit_id = new.unit_id);
|
|
126
|
+
select raise(abort, 'a quarantined or unsourced unit cannot become active')
|
|
127
|
+
where new.to_state = 'active' and exists (select 1 from unit where id = new.unit_id and (extraction <> 'supported' or unsourced = 1));
|
|
128
|
+
select raise(abort, 'an active decision or constraint needs unretracted evidence and adoption')
|
|
129
|
+
where new.to_state = 'active' and exists (select 1 from unit u where u.id = new.unit_id and u.kind in ('decision', 'constraint') and (
|
|
130
|
+
not exists (select 1 from unit_evidence e where e.unit_id = u.id and e.option_id is null and e.retracted_at is null)
|
|
131
|
+
or not exists (select 1 from unit_adoption a where a.unit_id = u.id and a.retracted_at is null)));
|
|
132
|
+
select raise(abort, 'an active implementation needs code or commit evidence')
|
|
133
|
+
where new.to_state = 'active' and exists (select 1 from unit u where u.id = new.unit_id and u.kind = 'implementation' and not (
|
|
134
|
+
exists (select 1 from unit_evidence e join source s on s.id = e.source_id where e.unit_id = u.id and e.option_id is null
|
|
135
|
+
and e.retracted_at is null and e.role = 'implements' and s.kind in ('commit_message', 'file_excerpt'))
|
|
136
|
+
or exists (select 1 from unit_anchor a where a.unit_id = u.id and a.retired_at is null and a.role = 'evidence'
|
|
137
|
+
and (a.commit_sha is not null or (a.edit_observation_id is not null and exists (select 1 from unit_evidence e
|
|
138
|
+
join source s on s.id = e.source_id join edit_observation o on o.id = a.edit_observation_id
|
|
139
|
+
where e.unit_id = u.id and e.option_id is null and e.retracted_at is null and e.role = 'implements'
|
|
140
|
+
and s.session_id = o.session_id))))));
|
|
141
|
+
select raise(abort, 'an active unit needs unretracted evidence')
|
|
142
|
+
where new.to_state = 'active' and exists (select 1 from unit u where u.id = new.unit_id and u.kind in ('finding', 'dead_end', 'question')
|
|
143
|
+
and not exists (select 1 from unit_evidence e where e.unit_id = u.id and e.option_id is null and e.retracted_at is null));
|
|
144
|
+
-- A reconsider condition is the owner's: each needs a quote of the owner, written when the unit is saved. A quote retracted later, or
|
|
145
|
+
-- forgotten (forget's recheck is exempt, since the row is gone), leaves the unit as it was, and readers show the condition as unsupported
|
|
146
|
+
select raise(abort, 'a reconsider condition needs a quote of the owner')
|
|
147
|
+
where new.to_state = 'active' and new.forget_id is null and exists (select 1 from unit_option o where o.unit_id = new.unit_id
|
|
148
|
+
and o.reconsider_when is not null and not exists (select 1 from unit_evidence e join source s on s.id = e.source_id
|
|
149
|
+
where e.option_id = o.id and e.role = 'reconsiders' and s.author_kind = 'owner'));
|
|
150
|
+
select raise(abort, 'superseded needs a supersedes link from its successor')
|
|
151
|
+
where new.to_state = 'superseded' and not exists (select 1 from unit_link where to_unit = new.unit_id and kind = 'supersedes');
|
|
152
|
+
end;
|
|
153
|
+
create view unit_search_text as
|
|
154
|
+
select u.id,
|
|
155
|
+
sphica_terms(u.text || char(10) || coalesce(u.why, '') || char(10) || coalesce(u.scope_note, '') || char(10)
|
|
156
|
+
|| coalesce(u.revisit_when, '') || char(10)
|
|
157
|
+
|| coalesce((select group_concat(o.text || ' ' || coalesce(o.why, '') || ' ' || coalesce(o.reconsider_when, ''), char(10))
|
|
158
|
+
from (select text, why, reconsider_when from unit_option where unit_id = u.id order by position) o), '')) as body,
|
|
159
|
+
sphica_terms(coalesce((select group_concat(a.path || ' ' || coalesce(a.symbol, ''), char(10))
|
|
160
|
+
from (select path, symbol from unit_anchor where unit_id = u.id and retired_at is null order by id) a), '')) as ident,
|
|
161
|
+
sphica_terms(coalesce((select group_concat(j.value, ' ')
|
|
162
|
+
from json_each((select terms from unit_alias where unit_id = u.id and content_hash = u.content_hash order by id desc limit 1)) j), ''))
|
|
163
|
+
as alias
|
|
164
|
+
from unit u;
|
|
165
|
+
create trigger unit_fts_ai after insert on unit begin
|
|
166
|
+
insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text where id = new.id;
|
|
167
|
+
end;
|
|
168
|
+
create trigger unit_fts_option_i after insert on unit_option begin
|
|
169
|
+
delete from unit_fts where rowid = new.unit_id;
|
|
170
|
+
insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text where id = new.unit_id;
|
|
171
|
+
end;
|
|
172
|
+
create trigger unit_fts_anchor_i after insert on unit_anchor begin
|
|
173
|
+
delete from unit_fts where rowid = new.unit_id;
|
|
174
|
+
insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text where id = new.unit_id;
|
|
175
|
+
end;
|
|
176
|
+
create trigger unit_fts_anchor_u after update on unit_anchor begin
|
|
177
|
+
delete from unit_fts where rowid = new.unit_id;
|
|
178
|
+
insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text where id = new.unit_id;
|
|
179
|
+
end;
|
|
180
|
+
create trigger unit_fts_anchor_d after delete on unit_anchor when exists (select 1 from unit where id = old.unit_id) begin
|
|
181
|
+
delete from unit_fts where rowid = old.unit_id;
|
|
182
|
+
insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text where id = old.unit_id;
|
|
183
|
+
end;
|
|
184
|
+
create trigger unit_fts_alias_i after insert on unit_alias begin
|
|
185
|
+
delete from unit_fts where rowid = new.unit_id;
|
|
186
|
+
insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text where id = new.unit_id;
|
|
187
|
+
end;
|
|
188
|
+
create trigger unit_fts_alias_d after delete on unit_alias when exists (select 1 from unit where id = old.unit_id) begin
|
|
189
|
+
delete from unit_fts where rowid = old.unit_id;
|
|
190
|
+
insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text where id = old.unit_id;
|
|
191
|
+
end;
|
|
192
|
+
|
|
193
|
+
pragma user_version = 3;
|
package/db/schema.sql
CHANGED
|
@@ -265,7 +265,10 @@ create table unit_option (
|
|
|
265
265
|
text text not null check (text <> ''),
|
|
266
266
|
outcome text not null check (outcome in ('chosen', 'rejected', 'deferred', 'proposed')),
|
|
267
267
|
why text,
|
|
268
|
+
-- For a rejected option, what the owner said would make it worth reconsidering; its words are a `reconsiders` evidence row on the option
|
|
269
|
+
reconsider_when text check (reconsider_when <> ''),
|
|
268
270
|
unique (unit_id, position),
|
|
271
|
+
check (reconsider_when is null or outcome = 'rejected'),
|
|
269
272
|
unique (unit_id, id)
|
|
270
273
|
) strict;
|
|
271
274
|
create trigger unit_option_sealed before insert on unit_option
|
|
@@ -288,7 +291,7 @@ create table unit_evidence (
|
|
|
288
291
|
source_id integer not null references source (id) on delete cascade,
|
|
289
292
|
span_start integer not null check (span_start >= 0),
|
|
290
293
|
span_end integer not null check (span_end > span_start),
|
|
291
|
-
role text not null check (role in ('states', 'proposes', 'rejects', 'explains', 'implements')),
|
|
294
|
+
role text not null check (role in ('states', 'proposes', 'rejects', 'explains', 'implements', 'reconsiders')),
|
|
292
295
|
-- A third party the owner reported ("X said ..."): hearsay by the owner, never X's own statement
|
|
293
296
|
reported_speaker text,
|
|
294
297
|
run_id integer not null references extraction_run (id),
|
|
@@ -412,6 +415,12 @@ create trigger unit_state_rules before insert on unit_state begin
|
|
|
412
415
|
select raise(abort, 'an active unit needs unretracted evidence')
|
|
413
416
|
where new.to_state = 'active' and exists (select 1 from unit u where u.id = new.unit_id and u.kind in ('finding', 'dead_end', 'question')
|
|
414
417
|
and not exists (select 1 from unit_evidence e where e.unit_id = u.id and e.option_id is null and e.retracted_at is null));
|
|
418
|
+
-- A reconsider condition is the owner's: each needs a quote of the owner, written when the unit is saved. A quote retracted later, or
|
|
419
|
+
-- forgotten (forget's recheck is exempt, since the row is gone), leaves the unit as it was, and readers show the condition as unsupported
|
|
420
|
+
select raise(abort, 'a reconsider condition needs a quote of the owner')
|
|
421
|
+
where new.to_state = 'active' and new.forget_id is null and exists (select 1 from unit_option o where o.unit_id = new.unit_id
|
|
422
|
+
and o.reconsider_when is not null and not exists (select 1 from unit_evidence e join source s on s.id = e.source_id
|
|
423
|
+
where e.option_id = o.id and e.role = 'reconsiders' and s.author_kind = 'owner'));
|
|
415
424
|
select raise(abort, 'superseded needs a supersedes link from its successor')
|
|
416
425
|
where new.to_state = 'superseded' and not exists (select 1 from unit_link where to_unit = new.unit_id and kind = 'supersedes');
|
|
417
426
|
end;
|
|
@@ -494,6 +503,10 @@ create trigger unit_evidence_check before insert on unit_evidence begin
|
|
|
494
503
|
select raise(abort, 'a reported speaker is the owner reporting someone else, so it must cite an owner session message')
|
|
495
504
|
where new.reported_speaker is not null and (trim(new.reported_speaker) = '' or not exists (select 1 from source
|
|
496
505
|
where id = new.source_id and kind = 'session_message' and author_kind = 'owner'));
|
|
506
|
+
select raise(abort, 'reconsiders quotes the owner on a rejected option that has a reconsider condition')
|
|
507
|
+
where new.role = 'reconsiders' and (new.option_id is null
|
|
508
|
+
or not exists (select 1 from unit_option where id = new.option_id and reconsider_when is not null)
|
|
509
|
+
or not exists (select 1 from source where id = new.source_id and author_kind = 'owner'));
|
|
497
510
|
end;
|
|
498
511
|
create trigger unit_evidence_retract before update on unit_evidence begin
|
|
499
512
|
select raise(abort, 'evidence is only ever retracted, once')
|
|
@@ -602,14 +615,14 @@ create trigger unit_rev_anchor_i after insert on unit_anchor begin update unit s
|
|
|
602
615
|
create trigger unit_rev_anchor_u after update on unit_anchor begin update unit set revision = revision + 1 where id = new.unit_id; end;
|
|
603
616
|
create trigger unit_rev_alias_i after insert on unit_alias begin update unit set revision = revision + 1 where id = new.unit_id; end;
|
|
604
617
|
|
|
605
|
-
-- Unit search text: body (text, reason, scope, revisit condition, options), identifiers (live anchors), and the newest matching alias set.
|
|
618
|
+
-- Unit search text: body (text, reason, scope, revisit condition, options and their reconsider conditions), identifiers (live anchors), and the newest matching alias set.
|
|
606
619
|
-- search.ts weighs body and identifiers above aliases. Lifecycle is not indexed; queries filter it.
|
|
607
620
|
create view unit_search_text as
|
|
608
621
|
select u.id,
|
|
609
622
|
sphica_terms(u.text || char(10) || coalesce(u.why, '') || char(10) || coalesce(u.scope_note, '') || char(10)
|
|
610
623
|
|| coalesce(u.revisit_when, '') || char(10)
|
|
611
|
-
|| coalesce((select group_concat(o.text || ' ' || coalesce(o.why, ''), char(10))
|
|
612
|
-
from (select text, why from unit_option where unit_id = u.id order by position) o), '')) as body,
|
|
624
|
+
|| coalesce((select group_concat(o.text || ' ' || coalesce(o.why, '') || ' ' || coalesce(o.reconsider_when, ''), char(10))
|
|
625
|
+
from (select text, why, reconsider_when from unit_option where unit_id = u.id order by position) o), '')) as body,
|
|
613
626
|
sphica_terms(coalesce((select group_concat(a.path || ' ' || coalesce(a.symbol, ''), char(10))
|
|
614
627
|
from (select path, symbol from unit_anchor where unit_id = u.id and retired_at is null order by id) a), '')) as ident,
|
|
615
628
|
sphica_terms(coalesce((select group_concat(j.value, ' ')
|
|
@@ -739,4 +752,4 @@ create trigger capture_delivery_insert instead of insert on capture_delivery beg
|
|
|
739
752
|
select last_insert_rowid(), j.value from json_each(coalesce(new.units, '[]')) j where true on conflict do nothing;
|
|
740
753
|
end;
|
|
741
754
|
|
|
742
|
-
pragma user_version =
|
|
755
|
+
pragma user_version = 3;
|
package/dist/capture.js
CHANGED
|
@@ -10813,7 +10813,7 @@ import os from "node:os";
|
|
|
10813
10813
|
import path from "node:path";
|
|
10814
10814
|
import { constants as C, DatabaseSync } from "node:sqlite";
|
|
10815
10815
|
var SCHEMA_GENERATION = 2;
|
|
10816
|
-
var SCHEMA_REVISION =
|
|
10816
|
+
var SCHEMA_REVISION = 3;
|
|
10817
10817
|
var sphicaHome = () => process.env.SPHICA_HOME || path.join(os.homedir(), ".sphica");
|
|
10818
10818
|
var dbFile = () => process.env.SPHICA_DB || path.join(sphicaHome(), "sphica.db");
|
|
10819
10819
|
function requireRuntime() {
|
package/dist/cli.js
CHANGED
|
@@ -14187,7 +14187,7 @@ import os from "node:os";
|
|
|
14187
14187
|
import path2 from "node:path";
|
|
14188
14188
|
import { constants as C, DatabaseSync } from "node:sqlite";
|
|
14189
14189
|
var SCHEMA_GENERATION = 2;
|
|
14190
|
-
var SCHEMA_REVISION =
|
|
14190
|
+
var SCHEMA_REVISION = 3;
|
|
14191
14191
|
var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
|
|
14192
14192
|
var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
|
|
14193
14193
|
function requireRuntime() {
|
package/dist/deliver.js
CHANGED
|
@@ -11086,7 +11086,7 @@ import os from "node:os";
|
|
|
11086
11086
|
import path2 from "node:path";
|
|
11087
11087
|
import { constants as C, DatabaseSync } from "node:sqlite";
|
|
11088
11088
|
var SCHEMA_GENERATION = 2;
|
|
11089
|
-
var SCHEMA_REVISION =
|
|
11089
|
+
var SCHEMA_REVISION = 3;
|
|
11090
11090
|
var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
|
|
11091
11091
|
var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
|
|
11092
11092
|
function requireRuntime() {
|
package/dist/mcp-record.js
CHANGED
|
@@ -45527,7 +45527,7 @@ import os from "node:os";
|
|
|
45527
45527
|
import path from "node:path";
|
|
45528
45528
|
import { constants as C, DatabaseSync } from "node:sqlite";
|
|
45529
45529
|
var SCHEMA_GENERATION = 2;
|
|
45530
|
-
var SCHEMA_REVISION =
|
|
45530
|
+
var SCHEMA_REVISION = 3;
|
|
45531
45531
|
var sphicaHome = () => process.env.SPHICA_HOME || path.join(os.homedir(), ".sphica");
|
|
45532
45532
|
var dbFile = () => process.env.SPHICA_DB || path.join(sphicaHome(), "sphica.db");
|
|
45533
45533
|
function requireRuntime() {
|
|
@@ -46091,7 +46091,14 @@ var UNIT_KINDS = [
|
|
|
46091
46091
|
];
|
|
46092
46092
|
var STANCES = ["do", "dont", "defer"];
|
|
46093
46093
|
var OPTION_OUTCOMES = ["chosen", "rejected", "deferred", "proposed"];
|
|
46094
|
-
var EVIDENCE_ROLES = [
|
|
46094
|
+
var EVIDENCE_ROLES = [
|
|
46095
|
+
"states",
|
|
46096
|
+
"proposes",
|
|
46097
|
+
"rejects",
|
|
46098
|
+
"explains",
|
|
46099
|
+
"implements",
|
|
46100
|
+
"reconsiders"
|
|
46101
|
+
];
|
|
46095
46102
|
var WORK_STATUSES = ["active", "blocked", "paused", "done", "abandoned"];
|
|
46096
46103
|
var HOSTS = ["claude-code", "codex"];
|
|
46097
46104
|
var sessionId = (projectId, host, externalId) => uuidFrom(String(projectId), host, externalId);
|
|
@@ -46131,11 +46138,15 @@ function readText(root, rel) {
|
|
|
46131
46138
|
return null;
|
|
46132
46139
|
if (!st.isFile() || st.size > MAX_BYTES)
|
|
46133
46140
|
return;
|
|
46134
|
-
|
|
46135
|
-
|
|
46141
|
+
try {
|
|
46142
|
+
const inside = path2.relative(fs2.realpathSync(root), fs2.realpathSync(abs));
|
|
46143
|
+
if (leaves(inside))
|
|
46144
|
+
return;
|
|
46145
|
+
const buf = fs2.readFileSync(abs);
|
|
46146
|
+
return buf.includes(0) ? undefined : buf.toString("utf8");
|
|
46147
|
+
} catch {
|
|
46136
46148
|
return;
|
|
46137
|
-
|
|
46138
|
-
return buf.includes(0) ? undefined : buf.toString("utf8");
|
|
46149
|
+
}
|
|
46139
46150
|
}
|
|
46140
46151
|
function findSymbol(text, symbol2) {
|
|
46141
46152
|
const re = new RegExp(`(?<![\\w$])${literal3(symbol2)}(?![\\w$])`);
|
|
@@ -47221,7 +47232,7 @@ var MAINTAINERS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
|
|
|
47221
47232
|
var text = (max) => exports_external.string().trim().min(1).max(max);
|
|
47222
47233
|
var Quote = exports_external.object({ source: exports_external.string().regex(SOURCE_REF, "cite a source ref such as s12"), quote: exports_external.string() }).strict();
|
|
47223
47234
|
var Evidence = Quote.extend({
|
|
47224
|
-
role: exports_external.enum(EVIDENCE_ROLES),
|
|
47235
|
+
role: exports_external.enum(EVIDENCE_ROLES).exclude(["reconsiders"]),
|
|
47225
47236
|
reported_speaker: text(100).optional()
|
|
47226
47237
|
}).strict();
|
|
47227
47238
|
var Unit = exports_external.object({
|
|
@@ -47237,6 +47248,8 @@ var Unit = exports_external.object({
|
|
|
47237
47248
|
text: text(500),
|
|
47238
47249
|
outcome: exports_external.enum(OPTION_OUTCOMES),
|
|
47239
47250
|
why: text(2000).optional(),
|
|
47251
|
+
reconsider_when: text(1000).optional(),
|
|
47252
|
+
reconsider_quote: Quote.optional(),
|
|
47240
47253
|
evidence: exports_external.array(Evidence.partial({ role: true })).max(10).default([])
|
|
47241
47254
|
}).strict()).max(12).default([]),
|
|
47242
47255
|
evidence: exports_external.array(Evidence).max(20).default([]),
|
|
@@ -47300,10 +47313,20 @@ async function checkRecord(db, target, raw) {
|
|
|
47300
47313
|
errors3.push(`${at}: stance is required for decisions and constraints, and only for them`);
|
|
47301
47314
|
if (u.revisit_when !== undefined && u.stance !== "defer")
|
|
47302
47315
|
errors3.push(`${at}: revisit_when goes only with stance defer`);
|
|
47316
|
+
for (const o of u.options) {
|
|
47317
|
+
if (o.reconsider_when === undefined !== (o.reconsider_quote === undefined))
|
|
47318
|
+
errors3.push(`${at}: option "${head(o.text, 60)}": reconsider_when and reconsider_quote go together`);
|
|
47319
|
+
else if (o.reconsider_when !== undefined && o.outcome !== "rejected")
|
|
47320
|
+
errors3.push(`${at}: option "${head(o.text, 60)}": a reconsider condition goes only on a rejected option`);
|
|
47321
|
+
}
|
|
47303
47322
|
}
|
|
47304
47323
|
const refs = new Set;
|
|
47305
47324
|
for (const u of record3.units)
|
|
47306
|
-
for (const q of [
|
|
47325
|
+
for (const q of [
|
|
47326
|
+
...u.evidence,
|
|
47327
|
+
...u.adoption,
|
|
47328
|
+
...u.options.flatMap((o) => [...o.evidence, ...o.reconsider_quote ? [o.reconsider_quote] : []])
|
|
47329
|
+
])
|
|
47307
47330
|
refs.add(Number(q.source.slice(1)));
|
|
47308
47331
|
const sources = new Map((refs.size ? await db.selectFrom("source").select(["id", "kind", "author_kind", "author_login", "author_association", "text"]).where("project_id", "=", target.projectId).where("id", "in", [...refs]).execute() : []).map((s) => [s.id, s]));
|
|
47309
47332
|
for (const r of refs) {
|
|
@@ -47342,12 +47365,30 @@ async function checkRecord(db, target, raw) {
|
|
|
47342
47365
|
];
|
|
47343
47366
|
});
|
|
47344
47367
|
const evidence = spans(u.evidence);
|
|
47368
|
+
const reconsider = (o) => {
|
|
47369
|
+
const q = o.reconsider_quote;
|
|
47370
|
+
const s = q && sources.get(Number(q.source.slice(1)));
|
|
47371
|
+
if (!q || !s || o.reconsider_when === undefined)
|
|
47372
|
+
return null;
|
|
47373
|
+
cites.add(s.id);
|
|
47374
|
+
if (s.author_kind !== "owner") {
|
|
47375
|
+
errors3.push(`${key}: option "${head(o.text, 60)}": reconsider_quote must quote the owner; ${q.source} is by ${s.author_login ?? s.author_kind}`);
|
|
47376
|
+
return null;
|
|
47377
|
+
}
|
|
47378
|
+
const span = locate2(s.text, q.quote);
|
|
47379
|
+
if (!span) {
|
|
47380
|
+
errors3.push(`${key}: option "${head(o.text, 60)}": reconsider_quote not found in ${q.source}: "${head(q.quote, 80)}"`);
|
|
47381
|
+
return null;
|
|
47382
|
+
}
|
|
47383
|
+
return { source: s.id, start: span[0], end: span[1] };
|
|
47384
|
+
};
|
|
47345
47385
|
const options = u.options.map((o) => ({
|
|
47346
47386
|
input: o,
|
|
47347
47387
|
evidence: spans(o.evidence.map((e) => ({
|
|
47348
47388
|
...e,
|
|
47349
47389
|
role: e.role ?? (o.outcome === "rejected" ? "rejects" : o.outcome === "chosen" ? "states" : "explains")
|
|
47350
|
-
})))
|
|
47390
|
+
}))),
|
|
47391
|
+
reconsider: reconsider(o)
|
|
47351
47392
|
}));
|
|
47352
47393
|
if (u.evidence.length === 0)
|
|
47353
47394
|
quarantine.push("no evidence cited");
|
|
@@ -47465,7 +47506,7 @@ var contentHash = (u) => sha256(JSON.stringify([
|
|
|
47465
47506
|
u.why ?? null,
|
|
47466
47507
|
u.scope_note ?? null,
|
|
47467
47508
|
u.revisit_when ?? null,
|
|
47468
|
-
u.options.map((o) => [o.text, o.outcome, o.why ?? null])
|
|
47509
|
+
u.options.map((o) => o.reconsider_when === undefined ? [o.text, o.outcome, o.why ?? null] : [o.text, o.outcome, o.why ?? null, o.reconsider_when])
|
|
47469
47510
|
]));
|
|
47470
47511
|
var ACTIVATION = /needs|cannot become active/;
|
|
47471
47512
|
async function saveRecord(trx, target, runId, checked, looked) {
|
|
@@ -47520,9 +47561,12 @@ ${checked.errors.map((e) => ` ${e}`).join(`
|
|
|
47520
47561
|
position: i + 1,
|
|
47521
47562
|
text: o.input.text,
|
|
47522
47563
|
outcome: o.input.outcome,
|
|
47523
|
-
why: o.input.why ?? null
|
|
47564
|
+
why: o.input.why ?? null,
|
|
47565
|
+
reconsider_when: o.input.reconsider_when ?? null
|
|
47524
47566
|
}).returning("id").executeTakeFirstOrThrow();
|
|
47525
47567
|
await evidence(o.evidence, option.id);
|
|
47568
|
+
if (o.reconsider)
|
|
47569
|
+
await evidence([{ ...o.reconsider, role: "reconsiders", reported: null }], option.id);
|
|
47526
47570
|
}
|
|
47527
47571
|
await evidence(p.evidence, null);
|
|
47528
47572
|
for (const a of p.adoption) {
|
|
@@ -47656,7 +47700,7 @@ var Op = exports_external.discriminatedUnion("op", [
|
|
|
47656
47700
|
source: SOURCE_REF2.optional(),
|
|
47657
47701
|
file: File2.optional(),
|
|
47658
47702
|
quote,
|
|
47659
|
-
role: exports_external.enum(EVIDENCE_ROLES),
|
|
47703
|
+
role: exports_external.enum(EVIDENCE_ROLES).exclude(["reconsiders"]),
|
|
47660
47704
|
reported_speaker: exports_external.string().trim().min(1).max(100).optional()
|
|
47661
47705
|
}).strict(),
|
|
47662
47706
|
exports_external.object({ op: exports_external.literal("adopt"), unit, revision, source: SOURCE_REF2, quote }).strict(),
|