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.
- package/README.md +7 -5
- package/commands/gcf.js +408 -73
- package/extension.json +1 -1
- package/hooks/pre-commit.js +47 -29
- package/injections/commit-status.md +1 -0
- package/injections/plan-context.md +3 -2
- package/injections/review-pillars.md +1 -1
- package/lib/anchors.js +110 -10
- package/lib/audit-core.js +175 -50
- package/lib/bundle.js +275 -0
- package/lib/context.js +55 -36
- package/lib/design-check.js +580 -0
- package/lib/design.js +179 -0
- package/lib/frontmatter.js +2 -1
- package/lib/gcf-paths.js +14 -3
- package/lib/git.js +85 -4
- package/lib/id.js +8 -14
- package/lib/index-gen.js +68 -16
- package/lib/ledger.js +92 -0
- package/lib/linter.js +91 -42
- package/lib/migrate.js +345 -0
- package/lib/playtest-list.js +1 -1
- package/lib/safe-path.js +23 -4
- package/lib/systems-doc.js +70 -0
- package/lib/transitions.js +1 -3
- package/lib/unit-ops.js +270 -0
- package/lib/validate-card.js +59 -14
- package/lib/verdict-fs.js +173 -96
- package/package.json +1 -1
- package/profiles/game.md +68 -0
- package/profiles/glossary.md +37 -0
- package/profiles/pillars.md +2 -1
- package/profiles/unit-constraint.md +42 -0
- package/profiles/unit-entity.md +38 -0
- package/profiles/unit-mechanic.md +106 -0
- package/profiles/unit-resource.md +86 -0
- package/profiles/unit-rule.md +42 -0
- package/schemas/card.schema.json +23 -7
- package/skills/gcf-adopt/SKILL.md +3 -3
- package/skills/gcf-audit/SKILL.md +4 -3
- package/skills/gcf-card/SKILL.md +26 -16
- package/skills/gcf-debate/SKILL.md +17 -14
- package/skills/gcf-ingest/SKILL.md +43 -16
- package/skills/gcf-init/SKILL.md +9 -8
- package/skills/gcf-verdict/SKILL.md +7 -6
- package/skills/gcf-vision/SKILL.md +17 -11
- package/templates/PILLARS.md +2 -0
- package/templates/backlog.md +2 -4
- package/templates/card.md +27 -10
- package/templates/cnlp.config.json +0 -1
- package/templates/design.md +59 -0
- package/templates/game.md +24 -0
- package/templates/glossary.md +11 -0
- package/templates/system.md +1 -1
- 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 {
|
|
5
|
-
import {
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
-
|
|
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`
|
|
59
|
-
*
|
|
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(
|
|
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 =
|
|
67
|
-
|
|
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)
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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}
|
|
193
|
+
const message = `gcf(verdict): ${verdict} ${cardId} → ${systems.join(',')}`;
|
|
194
|
+
/** @type {string | null} */
|
|
123
195
|
let hash = null;
|
|
124
|
-
await withFileRollback(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
package/profiles/game.md
ADDED
|
@@ -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
|
package/profiles/pillars.md
CHANGED
|
@@ -21,7 +21,8 @@ sections:
|
|
|
21
21
|
form: record-list
|
|
22
22
|
required: yes
|
|
23
23
|
record_keys: statement, invariant, review_question
|
|
24
|
-
|
|
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
|