gcf-aif-ext 0.2.7 → 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 +6 -4
- package/commands/gcf.js +190 -13
- package/extension.json +1 -1
- package/hooks/pre-commit.js +34 -30
- package/injections/commit-status.md +1 -0
- package/injections/plan-context.md +3 -2
- package/lib/anchors.js +36 -34
- package/lib/audit-core.js +56 -26
- package/lib/bundle.js +275 -0
- package/lib/context.js +3 -2
- package/lib/design-check.js +580 -0
- package/lib/design.js +179 -0
- package/lib/git.js +63 -0
- package/lib/id.js +7 -15
- package/lib/index-gen.js +42 -8
- package/lib/ledger.js +92 -0
- package/lib/linter.js +14 -3
- package/lib/migrate.js +345 -0
- package/lib/playtest-list.js +1 -1
- package/lib/systems-doc.js +70 -0
- package/lib/unit-ops.js +270 -0
- package/lib/validate-card.js +29 -18
- package/lib/verdict-fs.js +102 -88
- 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 +1 -1
- package/skills/gcf-audit/SKILL.md +4 -3
- package/skills/gcf-card/SKILL.md +25 -16
- package/skills/gcf-debate/SKILL.md +17 -14
- package/skills/gcf-ingest/SKILL.md +39 -23
- package/skills/gcf-init/SKILL.md +3 -3
- package/skills/gcf-verdict/SKILL.md +6 -5
- package/skills/gcf-vision/SKILL.md +17 -11
- package/templates/PILLARS.md +2 -0
- package/templates/backlog.md +2 -5
- 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/validate-card.js
CHANGED
|
@@ -3,35 +3,45 @@
|
|
|
3
3
|
const FIELDS = [
|
|
4
4
|
'schema',
|
|
5
5
|
'id',
|
|
6
|
-
'
|
|
6
|
+
'type',
|
|
7
|
+
'title',
|
|
7
8
|
'status',
|
|
8
|
-
'route',
|
|
9
9
|
'system',
|
|
10
10
|
'depends',
|
|
11
|
+
'pillars',
|
|
11
12
|
'source',
|
|
13
|
+
'reopens',
|
|
12
14
|
'tune_count',
|
|
13
15
|
'built_at',
|
|
14
16
|
'created',
|
|
15
17
|
];
|
|
16
|
-
const
|
|
18
|
+
const OPTIONAL = ['title', 'reopens', 'built_at'];
|
|
19
|
+
const REQUIRED = FIELDS.filter((f) => !OPTIONAL.includes(f));
|
|
20
|
+
const SLUG = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
21
|
+
const slugList = (v) => Array.isArray(v) && v.every((s) => SLUG.test(s));
|
|
22
|
+
const text = (v) => (typeof v === 'string' && v.length > 0) || 'must be a non-empty string';
|
|
17
23
|
|
|
18
24
|
const RULES = {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
// Schema 1 cards (`card-NNN` ids, `slug`, `route`) predate the Design Model: say how to move.
|
|
26
|
+
schema: (v) =>
|
|
27
|
+
v === 2 ||
|
|
28
|
+
(v === 1
|
|
29
|
+
? 'schema 1 card: rename the file to cards/<slug>.md, set id: <slug>, drop slug and route, ' +
|
|
30
|
+
'add type: mechanic and pillars: [], set schema: 2'
|
|
31
|
+
: 'schema must be 2'),
|
|
32
|
+
id: (v) => SLUG.test(v) || 'id must be a lowercase hyphen slug',
|
|
33
|
+
type: (v) =>
|
|
34
|
+
['mechanic', 'resource'].includes(v) ||
|
|
35
|
+
'type must be mechanic or resource (a hard unit never becomes a card)',
|
|
36
|
+
title: (v) => text(v) === true || `title ${text(v)}`,
|
|
22
37
|
status: (v) =>
|
|
23
38
|
['proposed', 'built', 'tuning'].includes(v) ||
|
|
24
39
|
`status "${v}" is not storable (П1: terminal statuses never live in a file)`,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
(Array.isArray(v) && v.every((d) => /^card-\d{3}$/.test(d))) ||
|
|
31
|
-
'depends must list card-NNN ids',
|
|
32
|
-
source: (v) =>
|
|
33
|
-
/^(GDD#[A-Za-z0-9_-]+|manual|inferred)$/.test(v) ||
|
|
34
|
-
'source must be GDD#<id>, manual or inferred',
|
|
40
|
+
system: (v) => (slugList(v) && v.length >= 1) || 'system must list >=1 subsystem slugs',
|
|
41
|
+
depends: (v) => slugList(v) || 'depends must list unit ids',
|
|
42
|
+
pillars: (v) => slugList(v) || 'pillars must list pillar ids',
|
|
43
|
+
source: (v) => /^(GDD#[A-Za-z0-9_-]+|manual)$/.test(v) || 'source must be GDD#<id> or manual',
|
|
44
|
+
reopens: (v) => text(v) === true || `reopens ${text(v)}`,
|
|
35
45
|
tune_count: (v) => (Number.isInteger(v) && v >= 0) || 'tune_count must be an integer >= 0',
|
|
36
46
|
built_at: (v) =>
|
|
37
47
|
v === null ||
|
|
@@ -43,11 +53,12 @@ const RULES = {
|
|
|
43
53
|
|
|
44
54
|
const dateStr = (v) => (v instanceof Date ? v.toISOString().slice(0, 10) : String(v));
|
|
45
55
|
|
|
46
|
-
const UNTIL_OK = /^\[until:\s*(verdict\(
|
|
56
|
+
const UNTIL_OK = /^\[until:\s*(verdict\([a-z0-9-]+\)|retune\([a-z0-9-]+\)|manual\([^)]*\))\]$/;
|
|
47
57
|
|
|
48
58
|
/** Validate card frontmatter. Returns a list of one-line errors (empty = valid). */
|
|
49
59
|
export function validateCard(data) {
|
|
50
60
|
const errors = [];
|
|
61
|
+
if (data.schema !== undefined && data.schema !== 2) return [RULES.schema(data.schema)];
|
|
51
62
|
for (const key of REQUIRED) if (!(key in data)) errors.push(`missing field: ${key}`);
|
|
52
63
|
for (const key of Object.keys(data)) {
|
|
53
64
|
if (!FIELDS.includes(key))
|
|
@@ -73,7 +84,7 @@ export function validateUntil(body) {
|
|
|
73
84
|
if (!UNTIL_OK.test(m[0]))
|
|
74
85
|
errors.push(
|
|
75
86
|
`line ${i + 1}: bad until annotation ${m[0]} ` +
|
|
76
|
-
'(use verdict(
|
|
87
|
+
'(use verdict(<id>), retune(system) or manual(text))',
|
|
77
88
|
);
|
|
78
89
|
}
|
|
79
90
|
});
|
package/lib/verdict-fs.js
CHANGED
|
@@ -1,79 +1,66 @@
|
|
|
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
|
-
// 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
|
-
];
|
|
13
|
+
const ENVELOPE = /^\S+\.\.\S+ /;
|
|
14
|
+
const real = (lines) => lines.filter((l) => !/^- none\b/.test(l));
|
|
20
15
|
|
|
21
|
-
function
|
|
22
|
-
const
|
|
16
|
+
function recordsOf(body, key) {
|
|
17
|
+
const { lines, sections } = parseBlocks(body);
|
|
18
|
+
const at = sections.find((s) => s.key === key && !s.value);
|
|
23
19
|
|
|
24
|
-
return
|
|
20
|
+
return at ? readRecords(itemsOf(lines, at.line)).records.map((r) => r.values) : [];
|
|
25
21
|
}
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
}
|
|
40
37
|
}
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
encoding: 'utf8',
|
|
55
|
-
});
|
|
56
|
-
if (gitDir.status !== 0) return null;
|
|
57
|
-
const dir = path.resolve(projectDir, gitDir.stdout.trim());
|
|
58
|
-
|
|
59
|
-
return IN_PROGRESS.find((name) => existsSync(path.join(dir, name))) ?? null;
|
|
60
|
-
}
|
|
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
|
+
}
|
|
61
51
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return
|
|
74
|
-
cwd: projectDir,
|
|
75
|
-
encoding: 'utf8',
|
|
76
|
-
}).stdout.trim();
|
|
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;
|
|
77
64
|
}
|
|
78
65
|
|
|
79
66
|
/** Find the card and refuse an unknown verdict or a transition the lifecycle forbids. */
|
|
@@ -95,51 +82,69 @@ async function findVerdictCard(root, cardId, verdict) {
|
|
|
95
82
|
function planTune({ card, note }) {
|
|
96
83
|
return async () => {
|
|
97
84
|
const data = { ...card.data, status: 'tuning', tune_count: (card.data.tune_count ?? 0) + 1 };
|
|
98
|
-
|
|
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;
|
|
99
87
|
await atomicWrite(card.file, serialize(data, body));
|
|
100
88
|
};
|
|
101
89
|
}
|
|
102
90
|
|
|
103
|
-
|
|
91
|
+
/** The graveyard line of a killed or dropped unit: `- <id> | <reason> | tags: <tags>`. */
|
|
92
|
+
export async function buryLine(root, id, reason, tags) {
|
|
104
93
|
const gyFile = path.join(root, 'graveyard.md');
|
|
105
|
-
|
|
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'));
|
|
106
121
|
|
|
107
122
|
return async () => {
|
|
108
|
-
|
|
109
|
-
const tagStr = (tags.length ? tags : systems).join(', ');
|
|
110
|
-
const line = `- ${cardId} | ${card.data.slug} | ${reason} | tags: ${tagStr}\n`;
|
|
111
|
-
const existing = existsSync(gyFile) ? await readFile(gyFile, 'utf8') : '';
|
|
112
|
-
await atomicWrite(gyFile, existing + line);
|
|
123
|
+
await buryLine(root, cardId, note, tags.length ? tags : systems);
|
|
113
124
|
await unlink(card.file);
|
|
114
125
|
};
|
|
115
126
|
}
|
|
116
127
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
: { data: { system }, body: 'purpose:\n- (fill in)\n' };
|
|
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);
|
|
121
131
|
let body = doc.body;
|
|
122
|
-
for (const [
|
|
123
|
-
body = upsertBlock(body, section,
|
|
132
|
+
for (const [section, lines] of Object.entries(collapseLines(card.body))) {
|
|
133
|
+
body = upsertBlock(body, section, lines);
|
|
124
134
|
}
|
|
125
135
|
|
|
126
|
-
|
|
127
|
-
const openLines = blockLines(card.body, 'interactions').filter((l) => l.includes('[until:'));
|
|
128
|
-
body = upsertBlock(body, 'open', openLines);
|
|
129
|
-
await atomicWrite(sysFile, serialize({ ...doc.data, updated_by: cardId }, body));
|
|
136
|
+
await atomicWrite(doc.file, serialize({ ...doc.data, updated_by: cardId }, body));
|
|
130
137
|
}
|
|
131
138
|
|
|
132
139
|
function planKeep({ root, card, cardId, systems, touched }) {
|
|
133
140
|
// `system:` names files, and the pre-commit gate validates it only after they are written:
|
|
134
141
|
// `../../x` would already have overwritten a file outside gcf/.
|
|
135
142
|
const sysFiles = systems.map((s) => resolveInside(path.join(root, 'systems'), `${s}.md`));
|
|
136
|
-
touched.push(...sysFiles);
|
|
143
|
+
touched.push(...sysFiles, path.join(root, 'history.md'));
|
|
137
144
|
|
|
138
145
|
return async () => {
|
|
139
|
-
for (const
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
146
|
+
for (const s of systems) await mergeIntoSystem(root, s, card, cardId);
|
|
147
|
+
await recordLine(root, cardId, 'collapsed');
|
|
143
148
|
await unlink(card.file);
|
|
144
149
|
};
|
|
145
150
|
}
|
|
@@ -149,8 +154,9 @@ const PLANS = { tune: planTune, kill: planKill, keep: planKeep };
|
|
|
149
154
|
/**
|
|
150
155
|
* The one operation that collapses a card (GCF-PRD §8). Atomic: file changes roll back
|
|
151
156
|
* on failure; the verdict commit has the fixed greppable format. `keep` merges into every
|
|
152
|
-
* touched systems doc; `kill` writes the graveyard line; `tune`
|
|
153
|
-
*
|
|
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.
|
|
154
160
|
*
|
|
155
161
|
* The commit runs INSIDE the rollback: it fires the pre-commit gate, which can reject the
|
|
156
162
|
* merged systems doc, and a card unlinked by a commit that never landed is a lost card.
|
|
@@ -175,8 +181,16 @@ export async function applyVerdict(
|
|
|
175
181
|
const touched = [card.file, path.join(root, 'INDEX.md')];
|
|
176
182
|
const systems = card.data.system ?? [];
|
|
177
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
|
+
);
|
|
178
192
|
|
|
179
|
-
const message = `gcf(verdict): ${verdict} ${cardId}
|
|
193
|
+
const message = `gcf(verdict): ${verdict} ${cardId} → ${systems.join(',')}`;
|
|
180
194
|
/** @type {string | null} */
|
|
181
195
|
let hash = null;
|
|
182
196
|
await withFileRollback(
|
|
@@ -184,7 +198,7 @@ export async function applyVerdict(
|
|
|
184
198
|
async () => {
|
|
185
199
|
await mutate();
|
|
186
200
|
await writeIndex(root);
|
|
187
|
-
if (commit) hash =
|
|
201
|
+
if (commit) hash = commitPaths(projectDir, message, touched);
|
|
188
202
|
},
|
|
189
203
|
{ gitCwd: commit ? path.resolve(projectDir) : null },
|
|
190
204
|
);
|
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
|