gcf-aif-ext 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +7 -5
  2. package/commands/gcf.js +408 -73
  3. package/extension.json +1 -1
  4. package/hooks/pre-commit.js +47 -29
  5. package/injections/commit-status.md +1 -0
  6. package/injections/plan-context.md +3 -2
  7. package/injections/review-pillars.md +1 -1
  8. package/lib/anchors.js +110 -10
  9. package/lib/audit-core.js +175 -50
  10. package/lib/bundle.js +275 -0
  11. package/lib/context.js +55 -36
  12. package/lib/design-check.js +580 -0
  13. package/lib/design.js +179 -0
  14. package/lib/frontmatter.js +2 -1
  15. package/lib/gcf-paths.js +14 -3
  16. package/lib/git.js +85 -4
  17. package/lib/id.js +8 -14
  18. package/lib/index-gen.js +68 -16
  19. package/lib/ledger.js +92 -0
  20. package/lib/linter.js +91 -42
  21. package/lib/migrate.js +345 -0
  22. package/lib/playtest-list.js +1 -1
  23. package/lib/safe-path.js +23 -4
  24. package/lib/systems-doc.js +70 -0
  25. package/lib/transitions.js +1 -3
  26. package/lib/unit-ops.js +270 -0
  27. package/lib/validate-card.js +59 -14
  28. package/lib/verdict-fs.js +173 -96
  29. package/package.json +1 -1
  30. package/profiles/game.md +68 -0
  31. package/profiles/glossary.md +37 -0
  32. package/profiles/pillars.md +2 -1
  33. package/profiles/unit-constraint.md +42 -0
  34. package/profiles/unit-entity.md +38 -0
  35. package/profiles/unit-mechanic.md +106 -0
  36. package/profiles/unit-resource.md +86 -0
  37. package/profiles/unit-rule.md +42 -0
  38. package/schemas/card.schema.json +23 -7
  39. package/skills/gcf-adopt/SKILL.md +3 -3
  40. package/skills/gcf-audit/SKILL.md +4 -3
  41. package/skills/gcf-card/SKILL.md +26 -16
  42. package/skills/gcf-debate/SKILL.md +17 -14
  43. package/skills/gcf-ingest/SKILL.md +43 -16
  44. package/skills/gcf-init/SKILL.md +9 -8
  45. package/skills/gcf-verdict/SKILL.md +7 -6
  46. package/skills/gcf-vision/SKILL.md +17 -11
  47. package/templates/PILLARS.md +2 -0
  48. package/templates/backlog.md +2 -4
  49. package/templates/card.md +27 -10
  50. package/templates/cnlp.config.json +0 -1
  51. package/templates/design.md +59 -0
  52. package/templates/game.md +24 -0
  53. package/templates/glossary.md +11 -0
  54. package/templates/system.md +1 -1
  55. package/profiles/card.md +0 -65
package/lib/verdict-fs.js CHANGED
@@ -1,130 +1,207 @@
1
1
  import { readFile, unlink } from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
3
  import path from 'node:path';
4
- import { spawnSync } from 'node:child_process';
5
- import { parse, serialize } from './frontmatter.js';
4
+ import { parseBlocks, itemsOf, readRecords } from 'cnlp-kit';
5
+ import { serialize } from './frontmatter.js';
6
6
  import { listCards } from './gcf-paths.js';
7
7
  import { atomicWrite, resolveInside, withFileRollback } from './safe-path.js';
8
8
  import { writeIndex } from './index-gen.js';
9
9
  import { isLegal } from './transitions.js';
10
+ import { operationInProgress, commitPaths, dirtyPaths } from './git.js';
11
+ import { blockLines, upsertBlock, openSystemDoc } from './systems-doc.js';
10
12
 
11
- // Slot → systems-doc section map (GCF-PRD §8.2). Deterministic merge: lines are
12
- // carried verbatim and deduplicated; the LLM compression of `behavior` happens in the
13
- // card BEFORE the verdict (the skill edits the card), not here.
14
- const MERGE_MAP = [
15
- ['behavior', 'behavior'],
16
- ['contract', 'code'],
17
- ['feel', 'feel'],
18
- ['constraints', 'constraints'],
19
- ];
20
-
21
- function blockLines(body, key) {
22
- const m = String(body).match(new RegExp(`^${key}:\\s*\\n((?:- .*\\n?)*)`, 'm'));
23
- return m ? m[1].split('\n').filter((l) => l.startsWith('- ')) : [];
13
+ const ENVELOPE = /^\S+\.\.\S+ /;
14
+ const real = (lines) => lines.filter((l) => !/^- none\b/.test(l));
15
+
16
+ function recordsOf(body, key) {
17
+ const { lines, sections } = parseBlocks(body);
18
+ const at = sections.find((s) => s.key === key && !s.value);
19
+
20
+ return at ? readRecords(itemsOf(lines, at.line)).records.map((r) => r.values) : [];
24
21
  }
25
22
 
26
- function upsertBlock(body, key, addLines) {
27
- if (!addLines.length) return body;
28
- const existing = new Set(blockLines(body, key));
29
- const fresh = addLines.filter((l) => !existing.has(l));
30
- if (!fresh.length) return body;
31
- const re = new RegExp(`(^${key}:\\s*\\n(?:- .*\\n?)*)`, 'm');
32
- if (re.test(body)) return body.replace(re, (blk) => `${blk.endsWith('\n') ? blk : `${blk}\n`}${fresh.join('\n')}\n`);
33
- return `${body.trimEnd()}\n\n${key}:\n${fresh.join('\n')}\n`;
23
+ const scalar = (body, key) => parseBlocks(body).sections.find((s) => s.key === key)?.value ?? '';
24
+
25
+ /** A resource rate: an envelope is a range of intent (feel), a comparison is exact (constraint). */
26
+ function rateLines(body, out) {
27
+ for (const [key, word] of [
28
+ ['sources', 'source'],
29
+ ['sinks', 'sink'],
30
+ ]) {
31
+ for (const r of recordsOf(body, key)) {
32
+ out.behavior.push(`- ${word} ${r.name}`);
33
+ if (ENVELOPE.test(`${r.rate} `)) out.feel.push(`- ${r.name} rate ${r.rate}`);
34
+ else out.constraints.push(`- ${r.rate}`);
35
+ }
36
+ }
34
37
  }
35
38
 
36
- // `git commit --only <paths>` is a partial commit, which git refuses outright while a merge
37
- // or a sequencer operation is in progress. Detect it up front so the failure has a reason.
38
- const IN_PROGRESS = ['MERGE_HEAD', 'CHERRY_PICK_HEAD', 'REVERT_HEAD', 'rebase-merge', 'rebase-apply'];
39
- function operationInProgress(projectDir) {
40
- const gitDir = spawnSync('git', ['rev-parse', '--git-dir'], { cwd: projectDir, encoding: 'utf8' });
41
- if (gitDir.status !== 0) return null;
42
- const dir = path.resolve(projectDir, gitDir.stdout.trim());
43
- return IN_PROGRESS.find((name) => existsSync(path.join(dir, name))) ?? null;
39
+ /**
40
+ * Card → systems-doc lines by the fixed map (GCF-PRD §8.2). Deterministic: a record becomes
41
+ * one line, a bullet stays itself, `- none:` placeholders carry nothing. A line keeps its
42
+ * `[until: …]` and stays in its own section, where the reader who needs it looks.
43
+ */
44
+ export function collapseLines(body) {
45
+ /** @type {Record<string, string[]>} */
46
+ const out = { purpose: [], code: [], behavior: [], feel: [], constraints: [] };
47
+ out.purpose.push(...real(blockLines(body, 'intent')));
48
+ for (const r of recordsOf(body, 'behavior')) {
49
+ out.behavior.push(`- ${r.input} -> ${r.state} -> ${r.feedback}`);
50
+ }
51
+
52
+ out.behavior.push(...real(blockLines(body, 'refusal')), ...real(blockLines(body, 'boundaries')));
53
+ const persistence = scalar(body, 'persistence');
54
+ if (persistence && !/^none\b/.test(persistence)) out.behavior.push(`- persists: ${persistence}`);
55
+ rateLines(body, out);
56
+ const like = scalar(body, 'feel_like');
57
+ if (like) out.feel.push(`- ${like}`);
58
+ for (const r of recordsOf(body, 'feel')) out.feel.push(`- ${r.name} ${r.envelope}`);
59
+ out.code.push(...real(blockLines(body, 'contract')));
60
+ out.constraints.push(...real(blockLines(body, 'constraints')));
61
+ out.constraints.push(...real(blockLines(body, 'interactions')));
62
+
63
+ return out;
44
64
  }
45
65
 
46
- function gitCommit(projectDir, message, addPaths) {
47
- // Stage and commit exactly the files this verdict touched: `git add -- gcf/` would stage a
48
- // user's unrelated edits under gcf/, and rollback only restores what it snapshotted.
49
- for (const p of addPaths) spawnSync('git', ['add', '--', p], { cwd: projectDir, stdio: 'ignore' });
50
- const res = spawnSync('git', ['commit', '-m', message, '--only', '--', ...addPaths], { cwd: projectDir, encoding: 'utf8' });
51
- if (res.status !== 0) throw new Error(`verdict commit failed: ${res.stderr || res.stdout}`);
52
- return spawnSync('git', ['rev-parse', 'HEAD'], { cwd: projectDir, encoding: 'utf8' }).stdout.trim();
66
+ /** Find the card and refuse an unknown verdict or a transition the lifecycle forbids. */
67
+ async function findVerdictCard(root, cardId, verdict) {
68
+ const card = (await listCards(root)).find((c) => c.data.id === cardId);
69
+ if (!card) throw new Error(`Card not found: ${cardId}`);
70
+ const target = { keep: 'validated', tune: 'tuning', kill: 'killed' }[verdict];
71
+ if (!target) throw new Error(`Unknown verdict: ${verdict}`);
72
+ if (!isLegal(card.data.status, target)) {
73
+ throw new Error(
74
+ `Illegal transition ${card.data.status} -> ${target} for ${cardId} ` +
75
+ '(verdict needs status=built)',
76
+ );
77
+ }
78
+
79
+ return card;
80
+ }
81
+
82
+ function planTune({ card, note }) {
83
+ return async () => {
84
+ const data = { ...card.data, status: 'tuning', tune_count: (card.data.tune_count ?? 0) + 1 };
85
+ // The remark goes to `tuning:`, the block the agent reads before the rebuild.
86
+ const body = note ? upsertBlock(card.body, 'tuning', [`- ${note}`]) : card.body;
87
+ await atomicWrite(card.file, serialize(data, body));
88
+ };
53
89
  }
54
90
 
91
+ /** The graveyard line of a killed or dropped unit: `- <id> | <reason> | tags: <tags>`. */
92
+ export async function buryLine(root, id, reason, tags) {
93
+ const gyFile = path.join(root, 'graveyard.md');
94
+ const existing = existsSync(gyFile) ? await readFile(gyFile, 'utf8') : '';
95
+ const line = `- ${id} | ${reason || 'no reason recorded'} | tags: ${tags.join(', ')}\n`;
96
+ await atomicWrite(gyFile, existing + line);
97
+ }
98
+
99
+ export const HISTORY_HEAD =
100
+ '# History — `- <id> | done|collapsed | <date>`, one line per unit built (gcf done) or ' +
101
+ 'card kept by a verdict; the move writes it in the commit that ends the life\n';
102
+
103
+ /**
104
+ * The history line of a unit built (`done`) or a card kept (`collapsed`). It goes in id order,
105
+ * not at the end: branches recording different ids touch different places of the file and merge
106
+ * cleanly, and a revert of the move takes out exactly its line.
107
+ */
108
+ export async function recordLine(root, id, event, date = new Date().toISOString().slice(0, 10)) {
109
+ const file = path.join(root, 'history.md');
110
+ const old = existsSync(file) ? await readFile(file, 'utf8') : '';
111
+ // A file kept with CRLF stays CRLF; an empty one starts from the header.
112
+ const eol = old.includes('\r\n') ? '\r\n' : '\n';
113
+ const lines = (old.trim() ? old : HISTORY_HEAD).replace(/\s+$/, '').split(/\r?\n/);
114
+ const at = lines.findIndex((l) => (l.match(/^- (\S+) \|/)?.[1] ?? '') > id);
115
+ lines.splice(at < 0 ? lines.length : at, 0, `- ${id} | ${event} | ${date}`);
116
+ await atomicWrite(file, `${lines.join(eol)}${eol}`);
117
+ }
118
+
119
+ function planKill({ root, card, cardId, note, tags, systems, touched }) {
120
+ touched.push(path.join(root, 'graveyard.md'));
121
+
122
+ return async () => {
123
+ await buryLine(root, cardId, note, tags.length ? tags : systems);
124
+ await unlink(card.file);
125
+ };
126
+ }
127
+
128
+ /** Merge the card into one systems doc; a new doc starts from design/'s purpose. */
129
+ async function mergeIntoSystem(root, system, card, cardId) {
130
+ const doc = await openSystemDoc(root, system);
131
+ let body = doc.body;
132
+ for (const [section, lines] of Object.entries(collapseLines(card.body))) {
133
+ body = upsertBlock(body, section, lines);
134
+ }
135
+
136
+ await atomicWrite(doc.file, serialize({ ...doc.data, updated_by: cardId }, body));
137
+ }
138
+
139
+ function planKeep({ root, card, cardId, systems, touched }) {
140
+ // `system:` names files, and the pre-commit gate validates it only after they are written:
141
+ // `../../x` would already have overwritten a file outside gcf/.
142
+ const sysFiles = systems.map((s) => resolveInside(path.join(root, 'systems'), `${s}.md`));
143
+ touched.push(...sysFiles, path.join(root, 'history.md'));
144
+
145
+ return async () => {
146
+ for (const s of systems) await mergeIntoSystem(root, s, card, cardId);
147
+ await recordLine(root, cardId, 'collapsed');
148
+ await unlink(card.file);
149
+ };
150
+ }
151
+
152
+ const PLANS = { tune: planTune, kill: planKill, keep: planKeep };
153
+
55
154
  /**
56
155
  * The one operation that collapses a card (GCF-PRD §8). Atomic: file changes roll back
57
156
  * on failure; the verdict commit has the fixed greppable format. `keep` merges into every
58
- * touched systems doc; `kill` writes the graveyard line; `tune` bumps the counter and
59
- * appends the human note to the feel slot.
157
+ * touched systems doc and writes the history line; `kill` writes the graveyard line; `tune`
158
+ * bumps the counter and
159
+ * appends the human note to the `tuning` block.
60
160
  *
61
161
  * The commit runs INSIDE the rollback: it fires the pre-commit gate, which can reject the
62
162
  * merged systems doc, and a card unlinked by a commit that never landed is a lost card.
63
163
  */
64
- export async function applyVerdict(projectDir, cardId, verdict, { note = '', tags = [], commit = true } = {}) {
164
+ export async function applyVerdict(
165
+ projectDir,
166
+ cardId,
167
+ verdict,
168
+ { note = '', tags = [], commit = true } = {},
169
+ ) {
65
170
  const root = path.join(path.resolve(projectDir), 'gcf');
66
- const card = (await listCards(root)).find((c) => c.data.id === cardId);
67
- if (!card) throw new Error(`Card not found: ${cardId}`);
68
- const target = { keep: 'validated', tune: 'tuning', kill: 'killed' }[verdict];
69
- if (!target) throw new Error(`Unknown verdict: ${verdict}`);
70
- if (!isLegal(card.data.status, target)) {
71
- throw new Error(`Illegal transition ${card.data.status} -> ${target} for ${cardId} (verdict needs status=built)`);
72
- }
171
+ const card = await findVerdictCard(root, cardId, verdict);
172
+
73
173
  // Before any write: refusing after the files changed would send a failed commit through the
74
174
  // rollback, and restoring a conflicted index entry marks the conflict resolved.
75
175
  const busy = commit ? operationInProgress(projectDir) : null;
76
- if (busy) throw new Error(`cannot record a verdict while ${busy} is in progress — finish or abort it first`);
176
+ if (busy)
177
+ throw new Error(
178
+ `cannot record a verdict while ${busy} is in progress — finish or abort it first`,
179
+ );
77
180
 
78
181
  const touched = [card.file, path.join(root, 'INDEX.md')];
79
182
  const systems = card.data.system ?? [];
80
- let mutate;
81
-
82
- if (verdict === 'tune') {
83
- mutate = async () => {
84
- const data = { ...card.data, status: 'tuning', tune_count: (card.data.tune_count ?? 0) + 1 };
85
- const body = note ? upsertBlock(card.body, 'feel', [`- retune: ${note}`]) : card.body;
86
- await atomicWrite(card.file, serialize(data, body));
87
- };
88
- } else if (verdict === 'kill') {
89
- const gyFile = path.join(root, 'graveyard.md');
90
- touched.push(gyFile);
91
- mutate = async () => {
92
- const reason = note || 'no reason recorded';
93
- const tagStr = (tags.length ? tags : systems).join(', ');
94
- const line = `- ${cardId} | ${card.data.slug} | ${reason} | tags: ${tagStr}\n`;
95
- const existing = existsSync(gyFile) ? await readFile(gyFile, 'utf8') : '';
96
- await atomicWrite(gyFile, existing + line);
97
- await unlink(card.file);
98
- };
99
- } else {
100
- // `system:` names files, and the pre-commit gate validates it only after they are written:
101
- // `../../x` would already have overwritten a file outside gcf/.
102
- const sysFiles = systems.map((s) => resolveInside(path.join(root, 'systems'), `${s}.md`));
103
- touched.push(...sysFiles);
104
- mutate = async () => {
105
- for (const [i, sysFile] of sysFiles.entries()) {
106
- let doc = existsSync(sysFile)
107
- ? parse(await readFile(sysFile, 'utf8'))
108
- : { data: { system: systems[i] }, body: 'purpose:\n- (fill in)\n' };
109
- let body = doc.body;
110
- for (const [slot, section] of MERGE_MAP) {
111
- body = upsertBlock(body, section, blockLines(card.body, slot));
112
- }
113
- // Lines with a live until: move to `open` (GCF-PRD §8.2.5).
114
- const openLines = blockLines(card.body, 'interactions').filter((l) => l.includes('[until:'));
115
- body = upsertBlock(body, 'open', openLines);
116
- await atomicWrite(sysFile, serialize({ ...doc.data, updated_by: cardId }, body));
117
- }
118
- await unlink(card.file);
119
- };
120
- }
183
+ const mutate = PLANS[verdict]({ root, card, cardId, note, tags, systems, touched });
184
+ // The commit takes whole files: a user's own uncommitted edit in a systems doc or the
185
+ // graveyard would ride along unseen. The card is the verdict's input and may be dirty.
186
+ const others = touched.filter((f) => f !== card.file && path.basename(f) !== 'INDEX.md');
187
+ const dirty = commit ? dirtyPaths(projectDir, others) : [];
188
+ if (dirty.length)
189
+ throw new Error(
190
+ `commit or stash these first, the verdict would carry them: ${dirty.join('; ')}`,
191
+ );
121
192
 
122
- const message = `gcf(verdict): ${verdict} ${cardId} ${card.data.slug} → ${systems.join(',')}`;
193
+ const message = `gcf(verdict): ${verdict} ${cardId} → ${systems.join(',')}`;
194
+ /** @type {string | null} */
123
195
  let hash = null;
124
- await withFileRollback(touched, async () => {
125
- await mutate();
126
- await writeIndex(root);
127
- if (commit) hash = gitCommit(projectDir, message, touched);
128
- }, { gitCwd: commit ? path.resolve(projectDir) : null });
196
+ await withFileRollback(
197
+ touched,
198
+ async () => {
199
+ await mutate();
200
+ await writeIndex(root);
201
+ if (commit) hash = commitPaths(projectDir, message, touched);
202
+ },
203
+ { gitCwd: commit ? path.resolve(projectDir) : null },
204
+ );
205
+
129
206
  return { verdict, card: cardId, systems, hash };
130
207
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gcf-aif-ext",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "Game Context Framework for AI Factory",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: game
3
+ description: The CNL-P profile of gcf/game.md — the vision, the scope and the milestones the design units are planned against.
4
+ ---
5
+
6
+ format: cnlp-format.md
7
+
8
+ mood: declarative
9
+
10
+ headings:
11
+ - none: the body is flat and the document is named by its fixed path
12
+
13
+ frontmatter_fields:
14
+ - none: the file is a singleton at a fixed path and carries no machine fields
15
+
16
+ lexicon_exempt:
17
+ - none: nothing is exempt in the document that bounds the scope
18
+
19
+ sections:
20
+ - key: logline
21
+ form: scalar
22
+ required: yes
23
+ note: the game in one line
24
+
25
+ - key: platforms
26
+ form: bullet-list
27
+ required: yes
28
+ note: platform and input, one per line
29
+
30
+ - key: audience
31
+ form: bullet-list
32
+ required: yes
33
+ note: who plays, one group per line
34
+
35
+ - key: references
36
+ form: bullet-list
37
+ required: no
38
+ note: "like X, not like Y", one reference per line
39
+
40
+ - key: scope_in
41
+ form: bullet-list
42
+ required: yes
43
+ note: what the game holds
44
+
45
+ - key: scope_out
46
+ form: bullet-list
47
+ required: yes
48
+ note: what the game does not hold, or not in this version
49
+
50
+ - key: milestones
51
+ form: record-list
52
+ required: yes
53
+ record_keys: name, goal
54
+ optional_record_keys: content
55
+ note: the closed vocabulary of a unit's milestone field; content is a volume written as a comparison
56
+
57
+ - key: provenance
58
+ form: record-list
59
+ required: no
60
+ record_keys: slot
61
+ optional_record_keys: src, quote, answer, assumed
62
+ note: per slot, a verbatim quote with its anchor when the file came from a GDD
63
+
64
+ custom_sections:
65
+ - none: a block that is not listed above belongs to a unit or to PILLARS.md
66
+
67
+ enforcement:
68
+ - `ai-factory gcf design check` resolves every unit's milestone against this file
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: glossary
3
+ description: The CNL-P profile of gcf/glossary.md — one word per concept, the synonyms it forbids, and the project's own units of measure.
4
+ ---
5
+
6
+ format: cnlp-format.md
7
+
8
+ mood: declarative
9
+
10
+ headings:
11
+ - none: the body is flat and the document is named by its fixed path
12
+
13
+ frontmatter_fields:
14
+ - none: the file is a singleton at a fixed path and carries no machine fields
15
+
16
+ lexicon_exempt:
17
+ - none: the glossary is the lexicon of the project
18
+
19
+ sections:
20
+ - key: terms
21
+ form: record-list
22
+ required: yes
23
+ record_keys: term, means
24
+ optional_record_keys: not
25
+ note: a term the human confirmed, what it means, and the words that may not name it
26
+
27
+ - key: units
28
+ form: bullet-list
29
+ required: no
30
+ note: units of measure the project adds to the closed list, one per line as "<unit>: <what it measures>"
31
+
32
+ custom_sections:
33
+ - none: a block that is not listed above belongs to a unit
34
+
35
+ enforcement:
36
+ - `ai-factory gcf design check` refuses a forbidden synonym in gcf/design/ and in a card (M4)
37
+ - quoted text is data: a GDD quote keeps the designer's own word
@@ -21,7 +21,8 @@ sections:
21
21
  form: record-list
22
22
  required: yes
23
23
  record_keys: statement, invariant, review_question
24
- note: pillar count <= 7 and the token budget are enforced by the gcf audit core, not the checker
24
+ optional_record_keys: id
25
+ note: pillar count <= 7 and the token budget are enforced by the gcf audit core; id is the slug a design unit cites in pillars:
25
26
 
26
27
  custom_sections:
27
28
  - none: a pillar that needs more than its three keys is not a pillar yet
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: unit-constraint
3
+ description: The CNL-P profile of a GCF design unit of type constraint — a platform, performance or size threshold, accepted by a measurement.
4
+ ---
5
+
6
+ format: cnlp-format.md
7
+
8
+ mood: declarative
9
+
10
+ headings:
11
+ - none: the body is flat; the `## <id>` line and the header fields above the blocks name the unit
12
+
13
+ frontmatter_fields:
14
+ - none: a constraint never becomes a card, so its header lives above the blocks in design/
15
+
16
+ lexicon_exempt:
17
+ - none: every block is read when the constraint is planned and measured
18
+
19
+ sections:
20
+ - key: threshold
21
+ form: bullet-list
22
+ required: yes
23
+ note: one comparison per line, "<subject> <op> <value> <unit>"
24
+
25
+ - key: measure
26
+ form: bullet-list
27
+ required: yes
28
+ note: how the threshold is measured and on what - device, build, scene
29
+
30
+ - key: provenance
31
+ form: record-list
32
+ required: no
33
+ record_keys: slot
34
+ optional_record_keys: src, quote, answer, assumed
35
+ note: per slot, a verbatim quote with its anchor, a human answer, or a D2 assumption
36
+
37
+ custom_sections:
38
+ - none: a block that is not listed above is prose looking for a home
39
+
40
+ enforcement:
41
+ - `ai-factory gcf design check`, over the units in gcf/design/
42
+ - the gcf pre-commit hook runs the same check and refuses a commit with a blocking finding
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: unit-entity
3
+ description: The CNL-P profile of a GCF design unit of type entity — the shape of a data noun before code holds it.
4
+ ---
5
+
6
+ format: cnlp-format.md
7
+
8
+ mood: declarative
9
+
10
+ headings:
11
+ - none: the body is flat; the `## <id>` line and the header fields above the blocks name the unit
12
+
13
+ frontmatter_fields:
14
+ - none: an entity never becomes a card, so its header lives above the blocks in design/
15
+
16
+ lexicon_exempt:
17
+ - none: every block is read when the entity is planned and coded
18
+
19
+ sections:
20
+ - key: fields
21
+ form: record-list
22
+ required: yes
23
+ record_keys: name, holds
24
+ note: one record per field - what it holds, never its allowed values, which belong to a rule
25
+
26
+ - key: provenance
27
+ form: record-list
28
+ required: no
29
+ record_keys: slot
30
+ optional_record_keys: src, quote, answer, assumed
31
+ note: per slot, a verbatim quote with its anchor, a human answer, or a D2 assumption
32
+
33
+ custom_sections:
34
+ - none: a block that is not listed above is prose looking for a home
35
+
36
+ enforcement:
37
+ - `ai-factory gcf design check`, over the units in gcf/design/
38
+ - the gcf pre-commit hook runs the same check and refuses a commit with a blocking finding
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: unit-mechanic
3
+ description: The CNL-P profile of a GCF design unit of type mechanic — one body for its section in design/<system>.md and for its card in cards/<id>.md.
4
+ ---
5
+
6
+ format: cnlp-format.md
7
+
8
+ mood: declarative
9
+
10
+ headings:
11
+ - none: the body is flat; in design/ the `## <id>` line and the header fields above the blocks name the unit, in cards/ the frontmatter does
12
+
13
+ frontmatter_fields:
14
+ - schema
15
+ - id
16
+ - type
17
+ - title
18
+ - status
19
+ - system
20
+ - depends
21
+ - pillars
22
+ - source
23
+ - reopens
24
+ - tune_count
25
+ - built_at
26
+ - created
27
+
28
+ lexicon_exempt:
29
+ - none: every block is read when the unit is planned, built or played
30
+
31
+ sections:
32
+ - key: intent
33
+ form: bullet-list
34
+ required: yes
35
+ note: why the mechanic exists, in the designer's words
36
+
37
+ - key: behavior
38
+ form: record-list
39
+ required: yes
40
+ record_keys: input, state, feedback
41
+ note: one record per chain - player input, the state it changes, the feedback the player sees
42
+
43
+ - key: refusal
44
+ form: bullet-list
45
+ required: yes
46
+ note: what happens when the action is not allowed - no resource, invalid target, already active
47
+
48
+ - key: boundaries
49
+ form: bullet-list
50
+ required: yes
51
+ note: zero, maximum, overflow, simultaneity; an exact limit is a comparison at the end of its line
52
+
53
+ - key: persistence
54
+ form: scalar
55
+ required: yes
56
+ note: what survives a reload, a session or a match
57
+
58
+ - key: feel_like
59
+ form: scalar
60
+ required: yes
61
+ note: the comparison "like X, not like Y"
62
+
63
+ - key: feel
64
+ form: record-list
65
+ required: yes
66
+ record_keys: name, envelope
67
+ note: one record per tunable value; an envelope "a..b <unit>" is the only legal range of intent
68
+
69
+ - key: contract
70
+ form: bullet-list
71
+ required: yes
72
+ note: card stage, absent in design/ - paths and entry points, never member lists or values; an architecture rule is a constraint
73
+
74
+ - key: interactions
75
+ form: bullet-list
76
+ required: yes
77
+ note: card stage, absent in design/ - "<other id>: how the two cross", one line per crossing
78
+
79
+ - key: constraints
80
+ form: bullet-list
81
+ required: yes
82
+ note: negative constraints, one prohibition per line, checkable by an analyzer or a reviewer
83
+
84
+ - key: miss_criterion
85
+ form: scalar
86
+ required: yes
87
+ note: the one question the game in hand answers; the human answers it at the verdict
88
+
89
+ - key: tuning
90
+ form: bullet-list
91
+ required: no
92
+ note: card stage only - one verdict remark per tune, read before the rebuild
93
+
94
+ - key: provenance
95
+ form: record-list
96
+ required: no
97
+ record_keys: slot
98
+ optional_record_keys: src, quote, answer, assumed
99
+ note: per slot, a verbatim quote with its anchor, a human answer, or a D2 assumption
100
+
101
+ custom_sections:
102
+ - none: a block that is not listed above is prose looking for a home
103
+
104
+ enforcement:
105
+ - `ai-factory gcf design check`, over the units in gcf/design/ and the cards in gcf/cards/
106
+ - the gcf pre-commit hook runs the same check and refuses a commit with a blocking finding