@zalom/plastic 1.0.0-beta.1 → 1.0.0-beta.2
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/PLASTIC.md +14 -5
- package/package.json +1 -1
- package/scripts/dashboard.rb +5 -3
- package/scripts/doctor.rb +71 -0
- package/scripts/lib/intent_validator.rb +79 -0
- package/scripts/new-intent +50 -1
- package/skills/creating-intent/SKILL.md +20 -8
- package/skills/creating-intent/evals/evals.json +56 -0
- package/skills/creating-intent/references/lifecycle.md +7 -4
- package/skills/linking-intents/SKILL.md +11 -6
- package/skills/linking-intents/references/zettelkasten.md +8 -3
- package/skills/managing-index/references/zettelkasten-linking.md +1 -1
package/PLASTIC.md
CHANGED
|
@@ -35,15 +35,21 @@ Identity and knowledge graph only. Nothing operational.
|
|
|
35
35
|
---
|
|
36
36
|
id: "4a1"
|
|
37
37
|
intent: "Short description of the desire"
|
|
38
|
-
sources: ["4a"] #
|
|
39
|
-
chain: ["4a1a"] # forward
|
|
38
|
+
sources: ["4a"] # direct ascendants: intents this was created from
|
|
39
|
+
chain: ["4a1a"] # forward: what this spawned and related successors
|
|
40
40
|
created: 2026-05-29
|
|
41
41
|
author: human # human | agent-name
|
|
42
42
|
tags: [plastic, architecture]
|
|
43
43
|
---
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
- `sources`
|
|
46
|
+
- `sources` (formative, must-load, acyclic) and `chain` (forward + relational, lighter,
|
|
47
|
+
may cycle) form the directed knowledge graph. Reciprocity is one-directional: every
|
|
48
|
+
`sources` edge has a reciprocal `chain` entry (I1), but `chain` may carry relational
|
|
49
|
+
entries with no reciprocal `sources` (I2), so the graph is not strictly symmetric.
|
|
50
|
+
- Context contract: load `sources` strongly (they are what the intent was built from);
|
|
51
|
+
traverse `chain` lightly for discovery. See
|
|
52
|
+
docs/concepts/how-plastic-sources-and-chains-intents.md for the full model.
|
|
47
53
|
- IDs use Luhmann's alternating convention: `1` → `1a` → `1a1` → `1a1a`
|
|
48
54
|
- Multiple branches increment: `1a`, `1b`, `1c`
|
|
49
55
|
|
|
@@ -155,8 +161,11 @@ Format: `ID--three-to-five-words` (all stores).
|
|
|
155
161
|
|
|
156
162
|
- **Branch (`14a`, `14b`)** — a sub-task, refinement, or direct continuation of the
|
|
157
163
|
parent. It cannot stand on its own; it only makes sense as part of the parent's work.
|
|
158
|
-
- **Root (`15`, `16`)
|
|
159
|
-
|
|
164
|
+
- **Root (`15`, `16`)**: an independent thought, even if inspired by another intent.
|
|
165
|
+
Reserve `sources` for true created-from provenance (intents this was built out of). An
|
|
166
|
+
independent intent merely related to or inspired by another carries NO `sources`; record
|
|
167
|
+
the relation on the PREDECESSOR's `chain` (and mirror it as a `[[id]]` wikilink in
|
|
168
|
+
`## Links`).
|
|
160
169
|
- **Rule of thumb:** if the intent could exist without its parent, it's a root.
|
|
161
170
|
|
|
162
171
|
## INDEX.md
|
package/package.json
CHANGED
package/scripts/dashboard.rb
CHANGED
|
@@ -223,15 +223,17 @@ def effort_of(rec, type)
|
|
|
223
223
|
end
|
|
224
224
|
|
|
225
225
|
# Value -> :high | :low (explicit frontmatter field wins).
|
|
226
|
-
# High is deliberately rare: an explicit stamp,
|
|
227
|
-
#
|
|
226
|
+
# High is deliberately rare: an explicit stamp, a human-authored root idea, or an intent
|
|
227
|
+
# that has SPAWNED follow-on work, i.e. a strategic theme the user owns. "Has spawned work"
|
|
228
|
+
# means a reciprocal (I1) edge: another intent lists this one in its `sources`, captured by
|
|
229
|
+
# `referenced`. A purely relational `chain` entry (D2, no reciprocal `sources`) does NOT
|
|
230
|
+
# count as spawned, so bare `chain` membership is not a high-value signal (intent 68).
|
|
228
231
|
def value_of(rec, referenced = {})
|
|
229
232
|
case rec[:value_field]
|
|
230
233
|
when "high" then return :high
|
|
231
234
|
when "low" then return :low
|
|
232
235
|
end
|
|
233
236
|
return :high if rec[:author] == "human" && root_intent?(rec[:id])
|
|
234
|
-
return :high unless rec[:chain].empty?
|
|
235
237
|
return :high if referenced[[rec[:scope], rec[:id]]]
|
|
236
238
|
:low
|
|
237
239
|
end
|
package/scripts/doctor.rb
CHANGED
|
@@ -473,9 +473,80 @@ class Doctor
|
|
|
473
473
|
)
|
|
474
474
|
end
|
|
475
475
|
|
|
476
|
+
# graph_invariants — cross-intent I1/I3/I4 checks (intent 68). I1/I3/I4 are
|
|
477
|
+
# defined within a single store's id space (bare ids resolve within the same
|
|
478
|
+
# store), so build the `nodes` map per scope and run validate_graph per scope.
|
|
479
|
+
# I2 asymmetry (a relational chain entry with no reciprocal sources) is NEVER
|
|
480
|
+
# flagged: validate_graph does not compute it.
|
|
481
|
+
checks.concat(graph_invariant_checks(intent_dirs))
|
|
482
|
+
|
|
483
|
+
checks
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# Build a per-scope `nodes` map and surface IntentValidator.validate_graph
|
|
487
|
+
# findings as warn-level checks. Scope-aware (the caller already filtered
|
|
488
|
+
# `intent_dirs` by scope), so a `global` id is not falsely flagged as a dangler
|
|
489
|
+
# when only a `project:` store is loaded, and vice versa.
|
|
490
|
+
def graph_invariant_checks(intent_dirs)
|
|
491
|
+
nodes_by_scope = Hash.new { |h, k| h[k] = {} }
|
|
492
|
+
intent_dirs.each do |d|
|
|
493
|
+
md_path = File.join(d[:path], "#{d[:name]}.md")
|
|
494
|
+
next unless File.exist?(md_path)
|
|
495
|
+
|
|
496
|
+
fm = parse_frontmatter(md_path)
|
|
497
|
+
next unless fm.is_a?(Hash) && fm["id"]
|
|
498
|
+
|
|
499
|
+
nodes_by_scope[d[:scope]][fm["id"].to_s] = {
|
|
500
|
+
sources: Array(fm["sources"]).map(&:to_s),
|
|
501
|
+
chain: Array(fm["chain"]).map(&:to_s),
|
|
502
|
+
}
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
i1 = []
|
|
506
|
+
i3 = []
|
|
507
|
+
i4 = []
|
|
508
|
+
nodes_by_scope.each_value do |nodes|
|
|
509
|
+
findings = IntentValidator.validate_graph(nodes)
|
|
510
|
+
i1.concat(findings[:i1])
|
|
511
|
+
i3.concat(findings[:i3])
|
|
512
|
+
i4.concat(findings[:i4])
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
checks = []
|
|
516
|
+
checks << graph_finding_check(
|
|
517
|
+
"graph_i1_reciprocity", i1,
|
|
518
|
+
"Every sources edge has its reciprocal chain entry (I1)",
|
|
519
|
+
"Run new-intent / the rebuild so each source intent's chain backlinks the child"
|
|
520
|
+
)
|
|
521
|
+
checks << graph_finding_check(
|
|
522
|
+
"graph_i3_disjoint", i3,
|
|
523
|
+
"No intent lists the same id in both sources and chain (I3)",
|
|
524
|
+
"Remove the overlapping id from either sources or chain"
|
|
525
|
+
)
|
|
526
|
+
checks << graph_finding_check(
|
|
527
|
+
"graph_i4_danglers", i4,
|
|
528
|
+
"Every sources/chain id resolves to a real intent (I4)",
|
|
529
|
+
"Fix or remove the dangling id reference"
|
|
530
|
+
)
|
|
476
531
|
checks
|
|
477
532
|
end
|
|
478
533
|
|
|
534
|
+
# One graph check: pass when `findings` is empty, otherwise warn (never fail, so
|
|
535
|
+
# an existing store does not turn red on a graph finding). I1/I4 are auto-fixable.
|
|
536
|
+
def graph_finding_check(name, findings, pass_message, fix_hint)
|
|
537
|
+
if findings.empty?
|
|
538
|
+
check(category: "conventions", name: name, status: "pass", message: pass_message)
|
|
539
|
+
else
|
|
540
|
+
check(
|
|
541
|
+
category: "conventions", name: name, status: "warn",
|
|
542
|
+
message: "#{findings.size} #{name} violation(s)",
|
|
543
|
+
details: findings,
|
|
544
|
+
fixable: name != "graph_i3_disjoint",
|
|
545
|
+
fix_hint: fix_hint
|
|
546
|
+
)
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
479
550
|
# --- Check category 3: Agent registration ---
|
|
480
551
|
|
|
481
552
|
def check_agent_registration(agent_key)
|
|
@@ -153,4 +153,83 @@ module IntentValidator
|
|
|
153
153
|
content = File.exist?(md_path) ? File.read(md_path) : nil
|
|
154
154
|
validate_content(content)
|
|
155
155
|
end
|
|
156
|
+
|
|
157
|
+
# PURE: cross-intent graph-shape invariants (intent 68). These need visibility
|
|
158
|
+
# over the whole intent set, so they live apart from the single-file born-complete
|
|
159
|
+
# helpers above (which must not drift). No file IO: the caller builds `nodes`.
|
|
160
|
+
#
|
|
161
|
+
# `nodes` is a Hash { id(String) => { sources: [ids], chain: [ids] } } for every
|
|
162
|
+
# intent in ONE store's id space. Returns { i1: [...], i3: [...], i4: [...] },
|
|
163
|
+
# each an array of human-readable finding strings.
|
|
164
|
+
#
|
|
165
|
+
# I2 (no false symmetry) is INTENTIONALLY not computed: a relational `chain` entry
|
|
166
|
+
# with no reciprocal `sources` is valid and must never be flagged.
|
|
167
|
+
def validate_graph(nodes)
|
|
168
|
+
nodes = normalize_nodes(nodes)
|
|
169
|
+
{ i1: graph_i1(nodes), i3: graph_i3(nodes), i4: graph_i4(nodes) }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Coerce node arrays to deduped String id lists; tolerate missing keys.
|
|
173
|
+
def normalize_nodes(nodes)
|
|
174
|
+
return {} unless nodes.is_a?(Hash)
|
|
175
|
+
|
|
176
|
+
nodes.each_with_object({}) do |(id, edges), acc|
|
|
177
|
+
edges = {} unless edges.is_a?(Hash)
|
|
178
|
+
acc[id.to_s] = {
|
|
179
|
+
sources: Array(edges[:sources] || edges["sources"]).map(&:to_s).uniq,
|
|
180
|
+
chain: Array(edges[:chain] || edges["chain"]).map(&:to_s).uniq,
|
|
181
|
+
}
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# An id is a cross-store reference (out of this store's scope) when it carries a
|
|
186
|
+
# `<store>:` prefix, mirroring how `valid_id?` accepts the prefix. Such refs are
|
|
187
|
+
# resolved outside this node set, so they are never danglers here.
|
|
188
|
+
def cross_store_ref?(id)
|
|
189
|
+
id.to_s.include?(":")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# I1 (formative reciprocity): for every B and every `s` in B.sources that resolves
|
|
193
|
+
# in this store, B must appear in s.chain. A `s` that does not resolve is an I4
|
|
194
|
+
# dangler, not an I1 violation, so it is skipped here.
|
|
195
|
+
def graph_i1(nodes)
|
|
196
|
+
findings = []
|
|
197
|
+
nodes.each do |b_id, edges|
|
|
198
|
+
edges[:sources].each do |s|
|
|
199
|
+
next if cross_store_ref?(s)
|
|
200
|
+
next unless nodes.key?(s)
|
|
201
|
+
|
|
202
|
+
findings << "#{b_id}.sources lists #{s} but #{s}.chain is missing #{b_id}" unless nodes[s][:chain].include?(b_id)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
findings
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# I3 (per-node disjoint): X.sources and X.chain must not overlap.
|
|
209
|
+
def graph_i3(nodes)
|
|
210
|
+
findings = []
|
|
211
|
+
nodes.each do |x_id, edges|
|
|
212
|
+
(edges[:sources] & edges[:chain]).each do |overlap|
|
|
213
|
+
findings << "#{x_id} lists #{overlap} in BOTH sources and chain"
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
findings
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# I4 (no danglers): every bare (same-store) id in any sources/chain must resolve
|
|
220
|
+
# to a node. Cross-store `<store>:<id>` refs resolve elsewhere and are not flagged.
|
|
221
|
+
def graph_i4(nodes)
|
|
222
|
+
findings = []
|
|
223
|
+
nodes.each do |id, edges|
|
|
224
|
+
%i[sources chain].each do |field|
|
|
225
|
+
edges[field].each do |ref|
|
|
226
|
+
next if cross_store_ref?(ref)
|
|
227
|
+
next if nodes.key?(ref)
|
|
228
|
+
|
|
229
|
+
findings << "#{id}.#{field} references #{ref} which resolves to no intent"
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
findings
|
|
234
|
+
end
|
|
156
235
|
end
|
package/scripts/new-intent
CHANGED
|
@@ -73,6 +73,39 @@ def render_tokens(text, tokens)
|
|
|
73
73
|
tokens.reduce(text) { |acc, (k, v)| acc.gsub("{{#{k}}}", v.to_s) }
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
# Add an id to a source intent's frontmatter `chain` array, idempotently (I1
|
|
77
|
+
# reciprocity: `child in parent.sources` => `parent.chain` gains `child`). A
|
|
78
|
+
# targeted edit of the `chain:` line only; the body (including `## Links`) is
|
|
79
|
+
# preserved byte-for-byte and no other frontmatter key is touched, so the file
|
|
80
|
+
# stays born-complete. Renders the array in flow style (`["a", "b"]`) to match
|
|
81
|
+
# templates/intent.md. No-op when the id is already present.
|
|
82
|
+
def add_to_chain(file_path, new_id)
|
|
83
|
+
return unless File.exist?(file_path)
|
|
84
|
+
content = File.read(file_path)
|
|
85
|
+
return unless content.start_with?("---")
|
|
86
|
+
|
|
87
|
+
parts = content.split("---", 3)
|
|
88
|
+
return unless parts.length >= 3
|
|
89
|
+
|
|
90
|
+
fm = parts[1]
|
|
91
|
+
chain_line = fm.lines.find { |l| l.match?(/\A\s*chain\s*:/) }
|
|
92
|
+
return unless chain_line
|
|
93
|
+
|
|
94
|
+
existing = fm.match(/\bchain\s*:\s*\[(.*?)\]/m)
|
|
95
|
+
ids =
|
|
96
|
+
if existing
|
|
97
|
+
existing[1].scan(/"([^"]*)"|'([^']*)'/).flatten.compact
|
|
98
|
+
else
|
|
99
|
+
[]
|
|
100
|
+
end
|
|
101
|
+
return if ids.include?(new_id)
|
|
102
|
+
|
|
103
|
+
ids << new_id
|
|
104
|
+
rendered = "chain: [#{ids.map { |i| "\"#{i}\"" }.join(", ")}]"
|
|
105
|
+
new_fm = fm.sub(/^\s*chain\s*:.*$/, rendered)
|
|
106
|
+
File.write(file_path, ["", new_fm, parts[2]].join("---"))
|
|
107
|
+
end
|
|
108
|
+
|
|
76
109
|
# Append a wikilink line under the file's `## Links` section, idempotently.
|
|
77
110
|
def append_link(file_path, link_line)
|
|
78
111
|
return unless File.exist?(file_path)
|
|
@@ -140,7 +173,23 @@ def main(argv)
|
|
|
140
173
|
intent_file = File.join(intent_dir, "#{id}--#{slug}.md")
|
|
141
174
|
File.write(intent_file, intent_body)
|
|
142
175
|
|
|
143
|
-
#
|
|
176
|
+
# 4a. I1 reciprocity: write the child's id into EACH source intent's frontmatter
|
|
177
|
+
# `chain` (the formative-reciprocity backlink), for BOTH the `--parent` and the
|
|
178
|
+
# `--sources` path. `sources` is the redundant-explicit set from step 3 (it already
|
|
179
|
+
# folds in `--parent`). Scope boundary: 68 fixes ONLY the frontmatter `chain`
|
|
180
|
+
# backlink; the `## Links` wikilink projection for the `--sources` path is intent 72.
|
|
181
|
+
sources.each do |src_id|
|
|
182
|
+
next if src_id.nil? || src_id.empty?
|
|
183
|
+
|
|
184
|
+
src_dir = Dir.glob(File.join(store, "#{src_id}--*")).find { |d| File.directory?(d) }
|
|
185
|
+
next unless src_dir
|
|
186
|
+
|
|
187
|
+
src_file = File.join(src_dir, "#{File.basename(src_dir)}.md")
|
|
188
|
+
add_to_chain(src_file, id)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# 4b. Reciprocal `## Links` wikilinks: forward link to parent + back-reference in
|
|
192
|
+
# the parent (kept parent-only; the `--sources` path's wikilink is intent 72's scope).
|
|
144
193
|
if opts[:parent] && !opts[:parent].empty?
|
|
145
194
|
parent_id = opts[:parent]
|
|
146
195
|
append_link(intent_file, "- [[#{parent_id}]]")
|
|
@@ -30,7 +30,7 @@ description: Use when new work begins, the user expresses a new goal, says "new
|
|
|
30
30
|
|
|
31
31
|
When creating a tactical intent in a project store:
|
|
32
32
|
- Read the project's `AGENTS.md` for project context and decisions
|
|
33
|
-
- Link back to the project's governing intent (from `projects.yml` `parent` field) via `sources`
|
|
33
|
+
- Link back to the project's governing intent (from `projects.yml` `parent` field) via `sources` (the project genuinely is formed from its founding intent, a true formative edge, reciprocated on the founding intent's `chain`)
|
|
34
34
|
- Add `[[global:<parent_ID>]]` backlink in `## Links`
|
|
35
35
|
- The intent's Folgezettel ID is scoped to the project store (run `folgezettel-id` against the project's store at `~/.plastic/projects/{slug}/store/`)
|
|
36
36
|
|
|
@@ -48,10 +48,17 @@ Having a "parent" in mind does NOT automatically mean branch. Choose by meaning:
|
|
|
48
48
|
|
|
49
49
|
- **Branch (`14a`, `14b`)**: a sub-task, refinement, or direct continuation. It only
|
|
50
50
|
makes sense as part of the parent's work. Pass `--parent <parent_id>`.
|
|
51
|
-
- **Root (`15`, `16`)**: an independent thought
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
- **Root (`15`, `16`)**: an independent thought. Two cases, decided by ORIGIN:
|
|
52
|
+
- **Created from another intent** (it emerged from that intent's lifecycle): make it a
|
|
53
|
+
root and set `--sources <ascendant_id>`. `sources` is reserved for true created-from /
|
|
54
|
+
direct-ascendant provenance (D1).
|
|
55
|
+
- **Merely related to / inspired by another intent** (it did NOT come out of that
|
|
56
|
+
intent's lifecycle): carry NO `--sources`. Record the relation on the PREDECESSOR's
|
|
57
|
+
`chain` instead, and mirror it as a `[[id]]` wikilink in `## Links` (the
|
|
58
|
+
related-but-not-spawned rule).
|
|
59
|
+
- **Rule of thumb:** if the intent could exist without its parent, make it a root; only set
|
|
60
|
+
`--sources` when it was genuinely created from / emerged from that intent's lifecycle.
|
|
61
|
+
Topic similarity alone is not a `sources` edge.
|
|
55
62
|
|
|
56
63
|
### 3. Determine Intent Properties
|
|
57
64
|
|
|
@@ -59,11 +66,16 @@ Ask or infer from context:
|
|
|
59
66
|
- **intent**: one-line description
|
|
60
67
|
- **slug**: short hyphenated handle for the directory name
|
|
61
68
|
- **author**: `human` | `claude-code` | other agent name
|
|
62
|
-
- **sources**:
|
|
63
|
-
|
|
69
|
+
- **sources**: the direct ascendant(s) this intent was created from / emerged from the
|
|
70
|
+
lifecycle of (formation, not topic similarity), e.g., `4a1`. For a project intent,
|
|
71
|
+
include the governing intent's id. A branch's structural parent is ALSO recorded in
|
|
72
|
+
`sources` (the ID carries it for the human/paper tree, `sources` carries it for
|
|
73
|
+
software), which `new-intent` does automatically (see `new-intent:126`).
|
|
64
74
|
- **tags**: freeform list (use `project-<name>` for project membership)
|
|
65
75
|
|
|
66
|
-
`chain`
|
|
76
|
+
`chain` carries what this intent spawns AND related-but-not-spawned successors it leads to;
|
|
77
|
+
it starts empty and is populated later. See
|
|
78
|
+
`docs/concepts/how-plastic-sources-and-chains-intents.md` for the full model.
|
|
67
79
|
Place the intent in `## Active` or `## Future` in INDEX.md (status is
|
|
68
80
|
convention-derived, not a frontmatter field).
|
|
69
81
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "plastic-creating-intent",
|
|
3
|
+
"notes": "Intent 68. Scope: output-quality for the sources-vs-chain construction rules (D1/D2). Asserts the related-but-not-spawned case produces NO sources plus a predecessor chain link and a ## Links mirror, contrasted with the created-from case (true ascendant -> --sources set, reciprocal chain). The machine-checkable half lives in test/new_intent_test.rb (test_sources_path_gets_child_in_chain_frontmatter); this file documents the agent-facing scenario for skill evaluation and is NOT run by bin/test.",
|
|
4
|
+
"evals": [
|
|
5
|
+
{
|
|
6
|
+
"id": 1,
|
|
7
|
+
"scope": "behavior",
|
|
8
|
+
"set": "train",
|
|
9
|
+
"prompt": "Create an intent for adding a retry policy to the uploader. It's related to intent 41 (the upload pipeline work) but it's independent: it did not come out of intent 41's lifecycle.",
|
|
10
|
+
"expected_output": "A new root intent is created with EMPTY sources (it was not created from 41). The relation is recorded on the PREDECESSOR: intent 41 gains the new intent's id in its frontmatter chain, and a [[<new-id>]] wikilink is added to intent 41's ## Links. The new intent is NOT given 41 in --sources (the related-but-not-spawned rule). No false symmetry: 41 keeps the new id on chain with no reciprocal sources.",
|
|
11
|
+
"files": [],
|
|
12
|
+
"assertions": [
|
|
13
|
+
{
|
|
14
|
+
"type": "code",
|
|
15
|
+
"check": "new intent sources is empty",
|
|
16
|
+
"observed": "sources: []",
|
|
17
|
+
"result": "pass"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"type": "code",
|
|
21
|
+
"check": "predecessor 41 chain includes new id",
|
|
22
|
+
"observed": "41.chain includes <new-id>",
|
|
23
|
+
"result": "pass"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"type": "code",
|
|
27
|
+
"check": "predecessor 41 ## Links has [[<new-id>]] mirror",
|
|
28
|
+
"observed": "[[<new-id>]] present in 41 ## Links",
|
|
29
|
+
"result": "pass"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": 2,
|
|
35
|
+
"scope": "behavior",
|
|
36
|
+
"set": "validation",
|
|
37
|
+
"prompt": "Create an intent that is the direct continuation of intent 41: it emerged from intent 41's lifecycle and could not exist without it.",
|
|
38
|
+
"expected_output": "Because the new intent was genuinely CREATED FROM 41 (D1), it carries 41 in --sources (or branches from 41, which folds 41 into sources via the redundant-explicit rule). The reciprocal I1 backlink lands: intent 41's frontmatter chain gains the new intent's id. This is the created-from case, contrasted with the related-but-not-spawned case in eval 1.",
|
|
39
|
+
"files": [],
|
|
40
|
+
"assertions": [
|
|
41
|
+
{
|
|
42
|
+
"type": "code",
|
|
43
|
+
"check": "new intent sources includes 41",
|
|
44
|
+
"observed": "sources includes 41",
|
|
45
|
+
"result": "pass"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"type": "code",
|
|
49
|
+
"check": "predecessor 41 chain includes new id (I1 reciprocity)",
|
|
50
|
+
"observed": "41.chain includes <new-id>",
|
|
51
|
+
"result": "pass"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
@@ -36,13 +36,16 @@ Never modified, only appended.
|
|
|
36
36
|
|
|
37
37
|
Tracks: stage transitions, decisions, shifts, blocks, cancellations, material for future intents.
|
|
38
38
|
This is how execution is tracked. When this intent completes, Insights
|
|
39
|
-
is where to look for what comes next. New intents spawned from
|
|
40
|
-
appear in the `chain`
|
|
39
|
+
is where to look for what comes next. New intents spawned from this one,
|
|
40
|
+
plus related-but-not-spawned successors it leads to, appear in the `chain`
|
|
41
|
+
field.
|
|
41
42
|
|
|
42
43
|
## `## Links`
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
The human-readable projection of the local knowledge graph: all `sources`
|
|
46
|
+
first (top, named), then all `chain` (named), as `[[id]]` wikilinks plus a
|
|
47
|
+
short label. Counterpart to the frontmatter `sources` / `chain` edges, for
|
|
48
|
+
Obsidian graph navigation.
|
|
46
49
|
|
|
47
50
|
## Conventions — Filesystem as Schema
|
|
48
51
|
|
|
@@ -21,13 +21,13 @@ Explicit wikilinks in the `## Links` section. Bidirectional — add to both inte
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
### 2. Sources (Backward)
|
|
24
|
-
The `sources` array in frontmatter.
|
|
24
|
+
The `sources` array in frontmatter. The direct ascendant(s) this intent was created from / emerged from the lifecycle of (formation, not topic similarity), backward links to the work it was built out of:
|
|
25
25
|
```yaml
|
|
26
26
|
sources: ["1a", "1a2"]
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
### 3. Chain (Forward)
|
|
30
|
-
The `chain` array in frontmatter. What this intent spawned
|
|
30
|
+
The `chain` array in frontmatter. What this intent spawned AND related-but-not-spawned successors it leads to, forward links to children, follow-on, and related work:
|
|
31
31
|
```yaml
|
|
32
32
|
chain: ["1b1", "1b2"]
|
|
33
33
|
```
|
|
@@ -55,16 +55,21 @@ done
|
|
|
55
55
|
|
|
56
56
|
### 2. Choose Connection Type
|
|
57
57
|
Ask the user which type of connection:
|
|
58
|
-
- **source
|
|
59
|
-
- **cross-reference
|
|
58
|
+
- **source**: "this was CREATED FROM that" (D1). The reciprocal update is one-directional (I1): add the ascendant id to this intent's `sources[]` AND add this intent's id to the ascendant's `chain[]`. A merely-related (not-created-from) connection is NOT a source: record it on the predecessor's `chain[]` only, plus a `## Links` wikilink, with NO `sources` (the related-but-not-spawned rule).
|
|
59
|
+
- **cross-reference**: "these are related" (add wikilink in `## Links` of both intents)
|
|
60
60
|
|
|
61
61
|
### 3. Apply Connection
|
|
62
62
|
|
|
63
|
-
**For sources:**
|
|
64
|
-
Update frontmatter arrays on both intents:
|
|
63
|
+
**For sources (a true created-from edge only):**
|
|
64
|
+
Update frontmatter arrays on both intents (I1, two-sided):
|
|
65
65
|
- Add the parent's ID to the child's `sources` array
|
|
66
66
|
- Add the child's ID to the parent's `chain` array
|
|
67
67
|
|
|
68
|
+
For the merely-related case, only the predecessor's `chain` (and both sides' `## Links`)
|
|
69
|
+
get the link, never `sources`. `chain` is NOT strictly the reverse of `sources` (I2):
|
|
70
|
+
relational `chain` entries are valid and must never be "corrected" by adding a reciprocal
|
|
71
|
+
`sources`.
|
|
72
|
+
|
|
68
73
|
**For cross-references:**
|
|
69
74
|
Add a wikilink in the `## Links` section of **both** intents (bidirectional).
|
|
70
75
|
|
|
@@ -20,9 +20,14 @@ IDs encode lineage using Luhmann's alternating convention:
|
|
|
20
20
|
|
|
21
21
|
## Knowledge Graph
|
|
22
22
|
|
|
23
|
-
`sources`
|
|
24
|
-
- `sources` =
|
|
25
|
-
|
|
23
|
+
`sources` and `chain` form the directed knowledge graph:
|
|
24
|
+
- `sources` = the direct ascendant(s) this intent was created from / emerged from the
|
|
25
|
+
lifecycle of (formation, not topic similarity); a DAG (acyclic), strong must-load context.
|
|
26
|
+
- `chain` = forward continuations AND related-but-not-spawned successors it leads to; a
|
|
27
|
+
directed graph that may cycle, lighter contributory context.
|
|
28
|
+
- Reciprocity is one-directional: every `sources` edge has a reciprocal `chain` entry (I1),
|
|
29
|
+
but `chain` may carry relational entries with no reciprocal `sources` (I2), so the graph is
|
|
30
|
+
NOT strictly double-linked.
|
|
26
31
|
|
|
27
32
|
## Dual-Mode
|
|
28
33
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
## Three Connection Types (Ranked)
|
|
10
10
|
|
|
11
11
|
1. **Direct links** (strongest) — wikilinks in `## Links` section
|
|
12
|
-
2. **Sources/Chain** (knowledge graph)
|
|
12
|
+
2. **Sources/Chain** (knowledge graph): `sources` = direct ascendants this was created from (formation, acyclic, must-load); `chain` = forward continuations and related successors (may cycle, lighter context). See `docs/concepts/how-plastic-sources-and-chains-intents.md` for the full model.
|
|
13
13
|
3. **Tags** (weakest) — shared tags, `project-<name>` for project membership
|
|
14
14
|
|
|
15
15
|
## When to Create a Cluster
|