Skip to content

Commit c635e46

Browse files
committed
feat: add project intent drift gate
1 parent cd7fbb5 commit c635e46

59 files changed

Lines changed: 3234 additions & 82 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/thesis-control/SKILL.md

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@ allowed-tools: Read, Glob, Grep, Edit, Write, Bash
88

99
## Purpose
1010

11-
Prevent AI-assisted writing from becoming fluent but distorted. Use this before and after substantive thesis edits when the risk is not spelling or style, but loss of author control: widened claims, blurred section purpose, missing caveats, unsynchronised adjacent paragraphs, or local edits that weaken the chapter spine.
11+
Prevent AI-assisted writing from becoming fluent but distorted. Use this before and after substantive thesis edits when the risk is not spelling or style, but loss of author control: project-level reframing, a primary domain becoming a secondary example, a changed research object or question, widened claims, blurred section purpose, missing caveats, unsynchronised adjacent paragraphs, or local edits that weaken the chapter spine.
1212

1313
## Trigger Words
1414

1515
This skill activates on: `thesis control`, `drift audit`, `edit contract`, `spine card`, `claim drift`, `author control`, `loss of control`, `scope creep`, `rewrite risk`, `/thesis-control`.
1616

1717
## Core Rule
1818

19-
Do not edit thesis prose until the intended change has an explicit contract.
19+
Do not edit thesis prose until the project intent, current manuscript contract,
20+
global thesis audit, section spine, and intended local change form one explicit
21+
and traceable contract chain.
2022

2123
The contract must answer:
2224

@@ -26,10 +28,30 @@ This edit is allowed to change [specific local issue] in [specific unit], while
2628

2729
If this sentence cannot be written, stop and diagnose the section instead of rewriting it.
2830

31+
The control hierarchy is:
32+
33+
```text
34+
Author-approved Project Intent
35+
→ Author-approved Manuscript Contract
36+
→ Passed Global Thesis Audit
37+
→ Section Spine Card
38+
→ Edit Contract
39+
→ Post-edit Drift Audit
40+
```
41+
42+
A lower layer cannot amend a higher one. If the title, abstract, primary
43+
domain, research object, research question, contribution scope, or manuscript
44+
structure no longer matches the approved intent, stop. Revise or roll back the
45+
manuscript, or create a new explicitly approved intent version that preserves
46+
the earlier row as history.
47+
2948
## Control Files
3049

3150
Use a `thesis_control/` directory when the project needs durable tracking:
3251

52+
- `project_intent.csv`
53+
- `manuscript_contracts.csv`
54+
- `global_thesis_audits.csv`
3355
- `spine_cards.csv`
3456
- `edit_contracts.csv`
3557
- `drift_audits.csv`
@@ -41,7 +63,16 @@ Run the optional validator when Python is available:
4163
python {skill_dir}/scripts/check_thesis_control.py <project_root> --strict
4264
```
4365

44-
The validator checks packet structure and gate consistency. It does not judge scholarly truth.
66+
Strict validation requires the project-intent layer. It blocks approved or
67+
applied edit contracts unless they reference a passed global thesis audit for
68+
the active author-approved intent and manuscript contract. Non-strict mode can
69+
still inspect legacy packets that do not yet have this layer.
70+
71+
The validator checks packet structure and recorded gate consistency. It does
72+
not infer semantic alignment or judge scholarly truth. The author or reviewer
73+
must compare the manuscript with the intent and record each alignment field
74+
honestly; the validator then prevents an unresolved or drifted audit from being
75+
used as authorisation.
4576

4677
Strict validation requires revision-tracking schema v3. Upgrade a complete
4778
legacy packet without guessing historical revision families:
@@ -62,10 +93,12 @@ python {skill_dir}/scripts/scaffold_thesis_control.py <project_root> \
6293
--copy-source
6394
```
6495

65-
The scaffold writes `human_approved=false`, `status=draft`, and
96+
The scaffold writes schema v4 draft project-intent and manuscript contracts, a
97+
pending global thesis audit, `human_approved=false`, `status=draft`, and
6698
`AUTHOR_REVIEW_REQUIRED` fields. Replace those fields with concrete author
67-
judgement before applying a substantive edit. Its default contract id includes
68-
the attempt number, so attempts 1 and 2 become `ec-<unit>-001` and
99+
judgement before applying a substantive edit. A scaffolded packet may be
100+
structurally valid while remaining non-executable. Its default contract id
101+
includes the attempt number, so attempts 1 and 2 become `ec-<unit>-001` and
69102
`ec-<unit>-002`. Reuse an explicit `revision_issue_id` for retries.
70103

71104
The migration helper stops without writing when revision metadata is partial or
@@ -74,8 +107,53 @@ resolved audits. It preserves named extension columns and converts one- or
74107
two-trigger legacy rows to `early_diagnostic`; a three-trigger row becomes a
75108
`cycle_gate` only when it already matches one completed failure group.
76109

110+
Upgrade a complete schema-v3 packet into a deliberately blocked schema-v4
111+
draft without guessing author intent:
112+
113+
```bash
114+
python {skill_dir}/scripts/upgrade_thesis_control_project_intent.py \
115+
<project_root> --json
116+
```
117+
118+
The helper adds `manuscript_id` and `global_audit_id` links, preserves named
119+
extension columns, and creates `AUTHOR_REVIEW_REQUIRED` draft intent,
120+
manuscript, and global-audit rows through one atomic batch. Previously approved
121+
or applied edits remain blocked. Replace the draft fields with real author
122+
judgement, approve the active intent and manuscript contract, and resolve the
123+
global audit before strict validation can pass. A partial project-intent schema
124+
stops without mutation.
125+
77126
## Workflow
78127

128+
### 0. Establish The Project Intent And Manuscript Contract
129+
130+
Before section-level planning, record:
131+
132+
- the primary scholarly domain
133+
- the research object
134+
- the core research question
135+
- the target venue or audience
136+
- concepts that must remain visible in the title or abstract
137+
- reframes that require fresh author approval
138+
- the current title, abstract focus, contribution scope, and structure
139+
- concrete approval evidence and the active version ids
140+
141+
Keep one active author-approved project intent and one active author-approved
142+
manuscript contract. A later intent version must identify the immediately
143+
previous version in `supersedes_intent_id`, record the amendment reason, and
144+
leave the earlier version as `superseded`. Do not overwrite the original row.
145+
146+
Run a global thesis audit whenever the title, abstract, primary domain,
147+
research object, research question, contribution scope, or overall structure
148+
changes. Record each dimension as `aligned`, `drifted`, or `not_assessed`.
149+
Only a fully aligned audit with `detected_reframe=false` can have
150+
`status=passed` and `human_decision=accept`.
151+
152+
If any dimension is `drifted`, set `human_review_required=true` and use
153+
`needs_review` or `failed`. The author must choose to revise the manuscript,
154+
roll back, or approve a versioned intent amendment. Merely accepting the audit
155+
cannot authorise the reframe.
156+
79157
### 1. Establish Or Read The Spine Card
80158

81159
Before editing a chapter, section, or paragraph cluster, identify:
@@ -87,6 +165,7 @@ Before editing a chapter, section, or paragraph cluster, identify:
87165
- scope boundary
88166
- core claims
89167
- do-not-change items
168+
- the active manuscript contract id
90169

91170
The spine sentence should be narrow:
92171

@@ -107,6 +186,7 @@ For every substantive edit, state:
107186
- adjacent context that must be checked
108187
- acceptance checks
109188
- whether human approval is required before editing
189+
- the passed global thesis audit id that covers the spine card's manuscript contract
110190

111191
When using the scaffold helper, treat its output as a draft control packet, not
112192
as approval. A generated contract becomes actionable only after the author has
@@ -120,10 +200,13 @@ Always require human approval for:
120200
- moving evidence between sections
121201
- rewriting more than one paragraph
122202
- merging or splitting sections
203+
- changing the title, abstract, primary domain, research object, research question, contribution scope, or manuscript structure
123204

124205
### 3. Apply Only Approved Changes
125206

126-
After approval, edit only the approved scope.
207+
After approval, edit only the approved scope. Do not apply the edit if the
208+
linked global thesis audit is pending, failed, stale, drifted, or attached to a
209+
different manuscript contract.
127210

128211
Keep mechanical fixes separate from argument changes. Do not bundle style, structure, evidence, and claim changes into one patch unless the contract explicitly allows it.
129212

@@ -285,3 +368,6 @@ Stop and ask for author direction if:
285368
- the user asks for a full-chapter rewrite without a spine map
286369
- previous AI edits cannot be distinguished from author-approved text
287370
- the edit would remove caveats, limitations, or uncertainty language without explicit approval
371+
- the active project intent or manuscript contract cannot be identified
372+
- a global thesis audit is missing, unresolved, stale, or records project-level drift
373+
- a proposed local contract would preserve a section spine that conflicts with the author-approved project intent

.claude/skills/thesis-control/scripts/check_thesis_control.py

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from pathlib import Path
1717
from typing import Dict, Iterable, List, Mapping, Optional, Sequence, Set, Tuple
1818

19+
from project_intent_control import validate_project_intent_layer
1920
from thesis_control_io import CsvShapeError, read_csv_table
2021

2122

@@ -203,6 +204,7 @@ def add_issue(issues: List[dict], kind: str, location: str, message: str) -> Non
203204
def validate_packet(
204205
root: Path,
205206
strict: bool = False,
207+
require_project_intent: Optional[bool] = None,
206208
table_overrides: Optional[
207209
Mapping[str, Tuple[Sequence[str], Sequence[Mapping[str, str]]]]
208210
] = None,
@@ -232,15 +234,27 @@ def validate_packet(
232234
column in contract_fields for column in REVISION_CONTRACT_COLUMNS
233235
)
234236

237+
intent_state, intent_issues = validate_project_intent_layer(
238+
root,
239+
required=strict if require_project_intent is None else require_project_intent,
240+
table_overrides=overrides,
241+
)
242+
intent_tracking = intent_state["enabled"]
243+
244+
spine_columns = list(REQUIRED_FILES["spine_cards.csv"])
245+
if intent_tracking:
246+
spine_columns.append("manuscript_id")
235247
spine_rows, spine_issues = validate_columns(
236248
root,
237249
"spine_cards.csv",
238-
REQUIRED_FILES["spine_cards.csv"],
250+
spine_columns,
239251
table=overrides.get("spine_cards.csv"),
240252
)
241253
contract_columns = list(REQUIRED_FILES["edit_contracts.csv"])
242254
if revision_tracking:
243255
contract_columns.extend(REVISION_CONTRACT_COLUMNS)
256+
if intent_tracking:
257+
contract_columns.append("global_audit_id")
244258
contract_rows, contract_issues = validate_columns(
245259
root,
246260
"edit_contracts.csv",
@@ -269,9 +283,16 @@ def validate_packet(
269283
allow_empty=True,
270284
table=overrides.get("revision_escalations.csv"),
271285
)
272-
issues.extend(spine_issues + contract_issues + audit_issues + escalation_issues)
286+
issues.extend(
287+
intent_issues
288+
+ spine_issues
289+
+ contract_issues
290+
+ audit_issues
291+
+ escalation_issues
292+
)
273293

274294
spine_ids = set()
295+
spine_manuscripts: Dict[str, str] = {}
275296
for index, row in enumerate(spine_rows):
276297
location = row_location("spine_cards.csv", index)
277298
unit_id = row.get("unit_id", "").strip()
@@ -299,6 +320,31 @@ def validate_packet(
299320
)
300321
if path_issue:
301322
add_issue(issues, "invalid-source-path", location, path_issue)
323+
if intent_tracking:
324+
manuscript_id = row.get("manuscript_id", "").strip()
325+
if not manuscript_id:
326+
add_issue(
327+
issues,
328+
"missing-manuscript-id",
329+
location,
330+
"spine card has no manuscript_id",
331+
)
332+
elif not is_valid_identifier(manuscript_id):
333+
add_issue(
334+
issues,
335+
"invalid-manuscript-id",
336+
location,
337+
"manuscript_id must be a safe identifier",
338+
)
339+
elif manuscript_id not in intent_state["manuscripts_by_id"]:
340+
add_issue(
341+
issues,
342+
"unknown-manuscript-id",
343+
location,
344+
f"spine card references unknown manuscript_id: {manuscript_id}",
345+
)
346+
if unit_id and manuscript_id:
347+
spine_manuscripts[unit_id] = manuscript_id
302348

303349
contract_ids = set()
304350
applied_contracts = set()
@@ -315,6 +361,7 @@ def validate_packet(
315361
approved = parse_bool(row.get("human_approved", ""))
316362
revision_issue_id = row.get("revision_issue_id", "").strip()
317363
attempt_text = row.get("attempt_no", "").strip()
364+
global_audit_id = row.get("global_audit_id", "").strip()
318365

319366
if not contract_id:
320367
add_issue(issues, "missing-contract-id", location, "edit contract has no contract_id")
@@ -350,6 +397,49 @@ def validate_packet(
350397
if status in {"approved", "applied"} and approved is not True:
351398
add_issue(issues, "missing-human-approval", location, "approved/applied contracts require human_approved=true")
352399

400+
if intent_tracking:
401+
if not global_audit_id:
402+
add_issue(
403+
issues,
404+
"missing-global-audit-id",
405+
location,
406+
"edit contract has no global_audit_id",
407+
)
408+
elif not is_valid_identifier(global_audit_id):
409+
add_issue(
410+
issues,
411+
"invalid-global-audit-id",
412+
location,
413+
"global_audit_id must be a safe identifier",
414+
)
415+
elif global_audit_id not in intent_state["audits_by_id"]:
416+
add_issue(
417+
issues,
418+
"unknown-global-audit-id",
419+
location,
420+
f"edit contract references unknown global_audit_id: {global_audit_id}",
421+
)
422+
else:
423+
audit_record = intent_state["audits_by_id"][global_audit_id]
424+
spine_manuscript = spine_manuscripts.get(unit_id)
425+
if spine_manuscript and audit_record["manuscript_id"] != spine_manuscript:
426+
add_issue(
427+
issues,
428+
"contract-global-audit-mismatch",
429+
location,
430+
"edit contract global audit does not cover the spine card's manuscript contract",
431+
)
432+
if (
433+
status in {"approved", "applied"}
434+
and global_audit_id not in intent_state["ready_audit_ids"]
435+
):
436+
add_issue(
437+
issues,
438+
"global-thesis-gate-required",
439+
location,
440+
"approved/applied edits require a passed global thesis audit for the active project intent and manuscript contract",
441+
)
442+
353443
for column in ["change_scope", "allowed_changes", "forbidden_changes", "adjacent_context", "acceptance_checks"]:
354444
if is_empty(row.get(column, "")):
355445
add_issue(issues, "empty-contract-field", location, f"edit contract field is empty: {column}")
@@ -759,6 +849,27 @@ def validate_packet(
759849
f"contract {contract_id} cannot proceed before revision issue {revision_issue_id} has an approved cycle_gate for {trigger_list}",
760850
)
761851

852+
executable_contracts = [
853+
contract_id
854+
for contract_id, status in contract_statuses.items()
855+
if status in {"approved", "applied"}
856+
]
857+
if intent_tracking and executable_contracts:
858+
if len(intent_state["active_intent_ids"]) != 1:
859+
add_issue(
860+
issues,
861+
"active-project-intent-required",
862+
"thesis_control/project_intent.csv",
863+
"approved/applied edits require exactly one active author-approved project intent",
864+
)
865+
if len(intent_state["active_manuscript_ids"]) != 1:
866+
add_issue(
867+
issues,
868+
"active-manuscript-contract-required",
869+
"thesis_control/manuscript_contracts.csv",
870+
"approved/applied edits require exactly one active author-approved manuscript contract",
871+
)
872+
762873
if strict:
763874
missing_audits = sorted(applied_contracts - audited_contracts)
764875
for contract_id in missing_audits:
@@ -780,6 +891,10 @@ def validate_packet(
780891
"revision_escalations": len(escalation_rows),
781892
"revision_tracking": revision_tracking,
782893
"escalation_schema_version": 3 if escalation_v3 else 2,
894+
"project_intent_tracking": intent_tracking,
895+
"project_intents": len(intent_state["intent_rows"]),
896+
"manuscript_contracts": len(intent_state["manuscript_rows"]),
897+
"global_thesis_audits": len(intent_state["audit_rows"]),
783898
},
784899
"issues": issues,
785900
"issue_count": len(issues),

0 commit comments

Comments
 (0)