gcf-aif-ext 0.5.0 → 0.5.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/README.md +11 -0
- package/extension.json +1 -1
- package/lib/design-check.js +24 -6
- package/lib/systems-doc.js +6 -2
- package/node_modules/cnlp-kit/CHANGELOG.md +207 -0
- package/node_modules/cnlp-kit/README.md +40 -3
- package/node_modules/cnlp-kit/bin/cnlp.js +71 -10
- package/node_modules/cnlp-kit/cnlp-format.md +36 -3
- package/node_modules/cnlp-kit/package.json +1 -1
- package/node_modules/cnlp-kit/profiles/docset.md +103 -103
- package/node_modules/cnlp-kit/skills/cnlp-docset/SKILL.md +6 -4
- package/node_modules/cnlp-kit/src/cnlp.js +71 -9
- package/node_modules/cnlp-kit/src/config.js +233 -15
- package/package.json +2 -2
- package/profiles/glossary.md +1 -1
- package/profiles/system.md +2 -2
- package/profiles/unit-constraint.md +3 -3
- package/profiles/unit-entity.md +3 -3
- package/profiles/unit-mechanic.md +6 -18
- package/profiles/unit-resource.md +6 -18
- package/profiles/unit-rule.md +3 -3
- package/skills/gcf-card/SKILL.md +3 -2
- package/templates/agents-block.md +1 -2
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
// inside a file the next upgrade overwrote.
|
|
5
5
|
|
|
6
6
|
import { existsSync } from 'node:fs';
|
|
7
|
-
import { glob, readdir, readFile } from 'node:fs/promises';
|
|
7
|
+
import { glob, readdir, readFile, realpath } from 'node:fs/promises';
|
|
8
8
|
import path from 'node:path';
|
|
9
|
-
import { bodyIssues, loadProfileAt, pageRefs, resolveDoc } from './cnlp.js';
|
|
9
|
+
import { bodyIssues, lexiconApplies, loadProfileAt, pageRefs, parseBlocks, resolveDoc } from './cnlp.js';
|
|
10
10
|
|
|
11
11
|
export const CONFIG_NAME = 'cnlp.config.json';
|
|
12
12
|
|
|
@@ -124,24 +124,48 @@ export function readingOrder(sets) {
|
|
|
124
124
|
return out;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/** `pageIdOf`, with the set renamed when the caller named it apart from its directory. */
|
|
128
|
+
const renamedIdOf = (rel, docsets, rename) => {
|
|
129
|
+
const id = pageIdOf(rel, docsets);
|
|
130
|
+
return id && { ...id, docset: rename[id.docset] ?? id.docset };
|
|
131
|
+
};
|
|
132
|
+
|
|
127
133
|
/**
|
|
128
134
|
* §9 reference conformance across a corpus, which is the 1 check no single document can make.
|
|
129
135
|
* `docs` is every file already read, so this adds no read of its own.
|
|
136
|
+
*
|
|
137
|
+
* `context` holds the pages of companion sets as `{ set, page, raw }`: they resolve references and
|
|
138
|
+
* are never checked. Their id comes with them rather than from a path, because a companion may sit
|
|
139
|
+
* outside the root, in a folder named by a version rather than by its set.
|
|
130
140
|
*/
|
|
131
|
-
export function linkIssues(docs, docsets) {
|
|
141
|
+
export function linkIssues(docs, docsets, { context = [], rename = {} } = {}) {
|
|
132
142
|
const pages = new Map();
|
|
133
143
|
const sets = new Set();
|
|
134
144
|
for (const { rel } of docs) {
|
|
135
|
-
const id =
|
|
145
|
+
const id = renamedIdOf(rel, docsets, rename);
|
|
136
146
|
if (!id) continue;
|
|
137
147
|
pages.set(`${id.docset}:${id.page}`, rel);
|
|
138
148
|
sets.add(id.docset);
|
|
139
149
|
}
|
|
150
|
+
const known = new Set(pages.keys());
|
|
151
|
+
for (const { set, page } of context) {
|
|
152
|
+
known.add(`${set}:${page}`);
|
|
153
|
+
sets.add(set);
|
|
154
|
+
}
|
|
140
155
|
|
|
141
156
|
const issues = [];
|
|
142
157
|
const reached = new Set();
|
|
158
|
+
// A companion's references are not this run's to report, but they count toward reach: a page of
|
|
159
|
+
// an integration set is reached only from the companion that overrides it.
|
|
160
|
+
for (const { set, page, raw } of context) {
|
|
161
|
+
for (const ref of pageRefs(raw)) {
|
|
162
|
+
if (ref.key === 'docset') continue;
|
|
163
|
+
const qualified = ref.target.includes(':') ? ref.target : `${set}:${ref.target}`;
|
|
164
|
+
if (qualified !== `${set}:${page}`) reached.add(qualified);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
143
167
|
for (const { rel, raw } of docs) {
|
|
144
|
-
const from =
|
|
168
|
+
const from = renamedIdOf(rel, docsets, rename);
|
|
145
169
|
if (!from) continue;
|
|
146
170
|
const self = `${from.docset}:${from.page}`;
|
|
147
171
|
for (const ref of pageRefs(raw)) {
|
|
@@ -157,7 +181,7 @@ export function linkIssues(docs, docsets) {
|
|
|
157
181
|
if (ref.key === 'by' && ref.target.includes(':')) {
|
|
158
182
|
issues.push({ file: rel, line: ref.line, message: `"by: ${ref.target}" carries a document set prefix — a "by:" value names a page of this set` });
|
|
159
183
|
}
|
|
160
|
-
if (!
|
|
184
|
+
if (!known.has(qualified)) {
|
|
161
185
|
issues.push({ file: rel, line: ref.line, message: `page "${qualified}" does not resolve to a file under ${docsets}/` });
|
|
162
186
|
} else if (qualified !== self) {
|
|
163
187
|
reached.add(qualified);
|
|
@@ -217,6 +241,85 @@ export async function skillDrift(root, agents, version) {
|
|
|
217
241
|
return out;
|
|
218
242
|
}
|
|
219
243
|
|
|
244
|
+
/**
|
|
245
|
+
* The standing rules `cnlp init` hands an operator to paste into the file their agent reads. 1
|
|
246
|
+
* value and not a string inside the command, because `cnlp check` reads it too: a pasted copy is a
|
|
247
|
+
* copy that goes stale, and nothing else in the project records that it was taken.
|
|
248
|
+
*/
|
|
249
|
+
export const AGENT_BLOCK = [
|
|
250
|
+
'- the CNL-P standard is the authority on document form: read it with `npx cnlp print format`',
|
|
251
|
+
'- run `npx cnlp check` before a commit that touches a document the corpus names',
|
|
252
|
+
"- load a library's documentation with `npx cnlp docs`, which writes every set's index in reading order",
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
// The 2 files an agent reads by convention, which are the 2 the corpus proposal below already
|
|
256
|
+
// knows. The block is pasted by hand, so nothing records which file took it.
|
|
257
|
+
const AGENT_FILES = ['AGENTS.md', 'CLAUDE.md'];
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The agent files carrying a copy of `AGENT_BLOCK` from an earlier version. A bullet naming
|
|
261
|
+
* `npx cnlp print format` is the block's own opener and is what finds the copy.
|
|
262
|
+
* ponytail: that phrase is the anchor — a version that stops naming it loses the watch on every
|
|
263
|
+
* copy already pasted, and the answer then is markers written into the file.
|
|
264
|
+
*/
|
|
265
|
+
export async function agentBlockDrift(root) {
|
|
266
|
+
const out = [];
|
|
267
|
+
for (const name of AGENT_FILES) {
|
|
268
|
+
const at = path.join(root, name);
|
|
269
|
+
if (!existsSync(at)) continue;
|
|
270
|
+
const lines = (await readFile(at, 'utf8')).split(/\r?\n/).map((l) => l.trimEnd());
|
|
271
|
+
const start = lines.findIndex((l) => /^-\s.*`npx cnlp print format`/.test(l));
|
|
272
|
+
if (start < 0) continue; // the project never took the block
|
|
273
|
+
if (lines.slice(start, start + AGENT_BLOCK.length).join('\n') !== AGENT_BLOCK.join('\n')) out.push(name);
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The local profiles a packaged profile of the same name has outgrown. A project copies a packaged
|
|
280
|
+
* profile to fill its own `custom_sections`, and `profileFor` reads the copy from then on: an
|
|
281
|
+
* upgrade moves the original underneath it, and the check reports a conformant corpus against a
|
|
282
|
+
* declaration this package has since changed. What a copy adds is the vocabulary it was copied for
|
|
283
|
+
* and is never drift; what the packaged profile declares and the copy does not is.
|
|
284
|
+
*/
|
|
285
|
+
export async function profileDrift(config) {
|
|
286
|
+
const out = [];
|
|
287
|
+
if (!config.profilesDir) return out;
|
|
288
|
+
for (const name of [...new Set(config.corpus.map((e) => e.profile))].sort()) {
|
|
289
|
+
const local = path.join(config.profilesDir, `${name}.md`);
|
|
290
|
+
const packagedAt = resolveDoc(name);
|
|
291
|
+
if (!existsSync(local) || !existsSync(packagedAt) || path.resolve(packagedAt) === path.resolve(local)) continue;
|
|
292
|
+
const packaged = await loadProfileAt(packagedAt);
|
|
293
|
+
const mine = await loadProfileAt(local);
|
|
294
|
+
const byKey = new Map(mine.sections.map((s) => [s.key, s]));
|
|
295
|
+
const missing = [];
|
|
296
|
+
for (const want of packaged.sections) {
|
|
297
|
+
const has = byKey.get(want.key);
|
|
298
|
+
if (!has) {
|
|
299
|
+
missing.push(`section "${want.key}"`);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (has.form !== want.form) missing.push(`section "${want.key}" as a ${want.form}`);
|
|
303
|
+
if (want.required && !has.required) missing.push(`section "${want.key}" as required`);
|
|
304
|
+
for (const k of want.recordKeys) {
|
|
305
|
+
if (!has.recordKeys.includes(k)) missing.push(`record key "${want.key}.${k}"`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
for (const h of packaged.headings) {
|
|
309
|
+
if (!mine.headings.includes(h)) missing.push(`heading "${h}"`);
|
|
310
|
+
}
|
|
311
|
+
for (const f of packaged.frontmatterFields) {
|
|
312
|
+
if (!mine.frontmatterFields.includes(f)) missing.push(`frontmatter field "${f}"`);
|
|
313
|
+
}
|
|
314
|
+
if (packaged.mood && packaged.mood !== mine.mood) missing.push(`mood "${packaged.mood}"`);
|
|
315
|
+
// Forward slashes, like every other path this reports: the checker names a file the same way
|
|
316
|
+
// on every platform.
|
|
317
|
+
const rel = path.relative(config.root, local).split(path.sep).join('/');
|
|
318
|
+
if (missing.length) out.push({ profile: name, file: rel, missing });
|
|
319
|
+
}
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
322
|
+
|
|
220
323
|
// The layouts a corpus entry is usually written for. `cnlp init` keeps the ones that match a file,
|
|
221
324
|
// so a project adopting the format starts from its own layout rather than from an example of one.
|
|
222
325
|
// ponytail: path convention only, never content — a decision record filed under docs/references
|
|
@@ -272,21 +375,69 @@ export async function proposeCorpus(root) {
|
|
|
272
375
|
* An entry matching no file is dropped here, which a written config is right to refuse: a glob a
|
|
273
376
|
* project typed and no file answers is a mistake, and a glob this function guessed is a folder the
|
|
274
377
|
* set does not have yet.
|
|
378
|
+
*
|
|
379
|
+
* 1 set checked alone still resolves its crossings: `companions` names the sets it crosses into,
|
|
380
|
+
* each `{ name, dir }`, and the sets beside it join as companions named by their directories. A
|
|
381
|
+
* companion is read to resolve a reference and never checked. `name` names the checked set when
|
|
382
|
+
* its directory does not, as in a layout of `<set>/<version>/`.
|
|
275
383
|
*/
|
|
276
|
-
export async function configForPath(dir) {
|
|
277
|
-
|
|
278
|
-
|
|
384
|
+
export async function configForPath(dir, { name, companions: named = [] } = {}) {
|
|
385
|
+
if (!existsSync(path.resolve(dir))) throw new Error(`no such directory: ${dir}`);
|
|
386
|
+
// The spelling on disk, not the one typed: on a case-insensitive disk `DragonECS-Unity` would
|
|
387
|
+
// otherwise miss its own directory among the siblings and read itself as a companion.
|
|
388
|
+
const abs = await realpath(path.resolve(dir));
|
|
389
|
+
if (!Array.isArray(named)) throw new Error(`companions is a list of { name, dir }, found ${JSON.stringify(named)}`);
|
|
279
390
|
let root;
|
|
280
391
|
let docsets = null;
|
|
281
392
|
let corpus;
|
|
282
|
-
|
|
393
|
+
const oneSet = existsSync(path.join(abs, 'index.md'));
|
|
394
|
+
// A root check already reads every set under it, and a project has no single set to name.
|
|
395
|
+
if (!oneSet && (name !== undefined || named.length)) {
|
|
396
|
+
throw new Error(`${abs} is not 1 document set — a name and companions apply to a directory holding an index.md`);
|
|
397
|
+
}
|
|
398
|
+
const base = path.basename(abs);
|
|
399
|
+
const setName = name ?? base;
|
|
400
|
+
const companions = [];
|
|
401
|
+
if (oneSet) {
|
|
402
|
+
if (name !== undefined) checkSetName(name, 'the checked set');
|
|
403
|
+
for (const c of named) {
|
|
404
|
+
checkSetName(c.name, 'a companion');
|
|
405
|
+
if (!c.dir) throw new Error(`companion "${c.name}" names no dir — give the directory of 1 document set`);
|
|
406
|
+
if (!existsSync(path.resolve(c.dir))) throw new Error(`companion "${c.name}": no such directory: ${path.resolve(c.dir)}`);
|
|
407
|
+
const at = await realpath(path.resolve(c.dir));
|
|
408
|
+
// The checked set read again under another name would reach its own pages through the copy.
|
|
409
|
+
if (at === abs) throw new Error(`companion "${c.name}" is the checked set itself — name another set`);
|
|
410
|
+
if (!existsSync(path.join(at, 'index.md'))) {
|
|
411
|
+
throw new Error(`companion "${c.name}": ${at} holds no index.md — name the directory of 1 document set`);
|
|
412
|
+
}
|
|
413
|
+
if (c.name === setName) throw new Error(`companion "${c.name}" carries the name of the checked set — name it apart`);
|
|
414
|
+
if (companions.some((o) => o.name === c.name)) throw new Error(`companion "${c.name}" is named twice — name each set once`);
|
|
415
|
+
companions.push({ name: c.name, dir: at });
|
|
416
|
+
}
|
|
417
|
+
// A set is present because a project vendors it, so the sets beside it are its companions.
|
|
418
|
+
// A name apart from the directory means no flat layout: the siblings there are other versions
|
|
419
|
+
// of the same set, and reading them as sets would resolve nothing.
|
|
420
|
+
// ponytail: a sibling's name is its directory's and is not held to `checkSetName`; a directory
|
|
421
|
+
// carrying ":" is possible only off Windows, and it would print as a set nobody can reference.
|
|
422
|
+
if (setName === base) {
|
|
423
|
+
const parent = path.dirname(abs);
|
|
424
|
+
for (const entry of (await readdir(parent, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
425
|
+
const at = path.join(parent, entry.name);
|
|
426
|
+
if (!entry.isDirectory() || entry.name === base || !existsSync(path.join(at, 'index.md'))) continue;
|
|
427
|
+
// An explicit companion wins, by name or by directory: 1 set is read once.
|
|
428
|
+
if (companions.some((o) => o.name === entry.name || o.dir === at)) continue;
|
|
429
|
+
companions.push({ name: entry.name, dir: at });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (oneSet) {
|
|
283
434
|
root = path.dirname(path.dirname(abs));
|
|
284
435
|
docsets = path.basename(path.dirname(abs));
|
|
285
|
-
corpus = docsetCorpus(`${docsets}/${
|
|
436
|
+
corpus = docsetCorpus(`${globLiteral(docsets)}/${globLiteral(base)}`);
|
|
286
437
|
} else if (await holdsASet(abs)) {
|
|
287
438
|
root = path.dirname(abs);
|
|
288
439
|
docsets = path.basename(abs);
|
|
289
|
-
corpus = docsetCorpus(`${docsets}/*`);
|
|
440
|
+
corpus = docsetCorpus(`${globLiteral(docsets)}/*`);
|
|
290
441
|
} else {
|
|
291
442
|
// Neither shape: fall back to the layouts `cnlp init` already knows, so pointing at a project
|
|
292
443
|
// reads the same as pointing at a set.
|
|
@@ -301,7 +452,29 @@ export async function configForPath(dir) {
|
|
|
301
452
|
if (kept.length === 0) {
|
|
302
453
|
throw new Error(`${abs} holds no document this kit knows the layout of — a document set is an index.md beside concepts, api and tasks`);
|
|
303
454
|
}
|
|
304
|
-
|
|
455
|
+
// `companions` is always present, `[]` included: a caller tells a kit that ignores the option
|
|
456
|
+
// from a set that failed by this field's absence.
|
|
457
|
+
const rename = setName === base ? {} : { [base]: setName };
|
|
458
|
+
return { path: null, root, profilesDir: null, docsets, corpus: kept, agents: [], companions, rename };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** A set name keys every page id, and `:` separates `<set>:<name>`, so a name carrying it reads as a page. */
|
|
462
|
+
function checkSetName(name, whose) {
|
|
463
|
+
if (typeof name !== 'string' || name === '' || /[:/\\]/.test(name)) {
|
|
464
|
+
throw new Error(`${JSON.stringify(name)} is no name for ${whose} — a set name is not empty and carries no ":", "/" or "\\"`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* A directory name as a glob that matches only itself. The corpus is a glob, and a set in
|
|
470
|
+
* `2.0.0-[rc]` otherwise reads as a character class that matches no directory, which reports
|
|
471
|
+
* the set as holding no document. A class of 1 character is the escape `fs.glob` honours: it
|
|
472
|
+
* reads `\` as a separator on Windows.
|
|
473
|
+
* ponytail: a `{` still opens a brace set, which no escape reaches through `fs.glob`; a set
|
|
474
|
+
* directory named with braces needs a config instead of a path.
|
|
475
|
+
*/
|
|
476
|
+
function globLiteral(name) {
|
|
477
|
+
return name.replace(/[[*?]/g, (c) => `[${c}]`);
|
|
305
478
|
}
|
|
306
479
|
|
|
307
480
|
/** Whether an immediate subdirectory carries an index.md, which is what makes a directory a root. */
|
|
@@ -329,10 +502,40 @@ export async function filesFor(entry, root) {
|
|
|
329
502
|
* Run every corpus entry. Returns the files checked and the issues found, each carrying the
|
|
330
503
|
* project-relative path it came from. Severity is the caller's: an issue is an issue here.
|
|
331
504
|
*/
|
|
505
|
+
/**
|
|
506
|
+
* The bytes a document set costs a reader, split by when they are paid. The index is loaded to
|
|
507
|
+
* answer any question and a page is loaded to answer 1, so the 2 numbers are different kinds of
|
|
508
|
+
* cost, and an author trimming the wrong one saves a reader nothing.
|
|
509
|
+
*
|
|
510
|
+
* Computed from the documents the caller has already read, so this adds no read of its own.
|
|
511
|
+
*/
|
|
512
|
+
export function setSizes(docs, docsets, rename = {}) {
|
|
513
|
+
const sets = new Map();
|
|
514
|
+
for (const { rel, raw } of docs) {
|
|
515
|
+
const id = renamedIdOf(rel, docsets, rename);
|
|
516
|
+
if (!id) continue;
|
|
517
|
+
const set = sets.get(id.docset) ?? { name: id.docset, indexBytes: 0, pages: 0, pageBytes: 0 };
|
|
518
|
+
const bytes = Buffer.byteLength(raw, 'utf8');
|
|
519
|
+
if (id.page === 'index') set.indexBytes += bytes;
|
|
520
|
+
else {
|
|
521
|
+
set.pages += 1;
|
|
522
|
+
set.pageBytes += bytes;
|
|
523
|
+
}
|
|
524
|
+
sets.set(id.docset, set);
|
|
525
|
+
}
|
|
526
|
+
return [...sets.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
527
|
+
}
|
|
528
|
+
|
|
332
529
|
export async function checkProject(config) {
|
|
333
530
|
const issues = [];
|
|
334
531
|
const checked = [];
|
|
335
532
|
const docs = [];
|
|
533
|
+
// A document the §5 word lists never reach. It is not an issue — nothing in it is wrong — but a
|
|
534
|
+
// run that says nothing about it reports a conformant corpus over rules that never ran.
|
|
535
|
+
const foreign = [];
|
|
536
|
+
// §3: a marked region the file's profile did not shape. Reported for the same reason: a run that
|
|
537
|
+
// names no exemption reads as a check of every line, and a region is an exemption anybody can write.
|
|
538
|
+
const regions = [];
|
|
336
539
|
for (const entry of config.corpus) {
|
|
337
540
|
const files = await filesFor(entry, config.root);
|
|
338
541
|
if (files.length === 0) {
|
|
@@ -343,10 +546,25 @@ export async function checkProject(config) {
|
|
|
343
546
|
const raw = await readFile(path.join(config.root, rel), 'utf8');
|
|
344
547
|
checked.push(rel);
|
|
345
548
|
docs.push({ rel, raw });
|
|
549
|
+
if (!lexiconApplies(raw)) foreign.push(rel);
|
|
550
|
+
for (const r of parseBlocks(raw).regions) regions.push({ file: rel, ...r });
|
|
346
551
|
for (const issue of bodyIssues(raw, profile)) issues.push({ file: rel, ...issue });
|
|
347
552
|
}
|
|
348
553
|
}
|
|
554
|
+
// A config from `readConfig` carries neither field: it already reads every set under its root.
|
|
555
|
+
const rename = config.rename ?? {};
|
|
556
|
+
// A companion's pages resolve references and are counted nowhere: the run reports on the set it
|
|
557
|
+
// was pointed at. `docsetCorpus` is the 1 place the shape of a set is written, so it is read here too.
|
|
558
|
+
const context = [];
|
|
559
|
+
for (const { name, dir } of config.companions ?? []) {
|
|
560
|
+
for (const entry of docsetCorpus('.')) {
|
|
561
|
+
for (const rel of await filesFor(entry, dir)) {
|
|
562
|
+
const page = rel.split(path.sep).join('/').slice(0, -3);
|
|
563
|
+
context.push({ set: name, page, raw: await readFile(path.join(dir, rel), 'utf8') });
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
349
567
|
// §9 is the 1 rule that needs the whole corpus, so it runs once over the files already read.
|
|
350
|
-
if (config.docsets) issues.push(...linkIssues(docs, config.docsets));
|
|
351
|
-
return { checked, issues };
|
|
568
|
+
if (config.docsets) issues.push(...linkIssues(docs, config.docsets, { context, rename }));
|
|
569
|
+
return { checked, issues, foreign, regions, sets: config.docsets ? setSizes(docs, config.docsets, rename) : [] };
|
|
352
570
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gcf-aif-ext",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Game Context Framework for AI Factory",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"README.md"
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"cnlp-kit": "^3.1
|
|
45
|
+
"cnlp-kit": "^3.9.1",
|
|
46
46
|
"yaml": "^2.5.1"
|
|
47
47
|
},
|
|
48
48
|
"bundleDependencies": [
|
package/profiles/glossary.md
CHANGED
|
@@ -33,5 +33,5 @@ custom_sections:
|
|
|
33
33
|
- none: a block that is not listed above belongs to a unit
|
|
34
34
|
|
|
35
35
|
enforcement:
|
|
36
|
-
- `ai-factory gcf design check` refuses a forbidden synonym in gcf/design/ and in a
|
|
36
|
+
- `ai-factory gcf design check` refuses a forbidden synonym in gcf/design/ and in a work item (M4); an unplayed item's frozen body only reports it
|
|
37
37
|
- quoted text is data: a GDD quote keeps the designer's own word
|
package/profiles/system.md
CHANGED
|
@@ -41,7 +41,7 @@ sections:
|
|
|
41
41
|
- key: constraints
|
|
42
42
|
form: bullet-list
|
|
43
43
|
required: yes
|
|
44
|
-
note: negative constraints and invariants accumulated from
|
|
44
|
+
note: negative constraints and invariants accumulated from work items and hard implementations
|
|
45
45
|
|
|
46
46
|
- key: open
|
|
47
47
|
form: bullet-list
|
|
@@ -49,7 +49,7 @@ sections:
|
|
|
49
49
|
note: lines carrying a live "[until: ...]" annotation, moved here by collapse
|
|
50
50
|
|
|
51
51
|
custom_sections:
|
|
52
|
-
- none: a fact that fits no block above belongs to code or to a
|
|
52
|
+
- none: a fact that fits no block above belongs to code or to a work item, not here
|
|
53
53
|
|
|
54
54
|
enforcement:
|
|
55
55
|
- `npx --yes cnlp-kit check`, over the files a `cnlp.config.json` corpus entry maps to this profile
|
|
@@ -8,10 +8,10 @@ format: cnlp-format.md
|
|
|
8
8
|
mood: declarative
|
|
9
9
|
|
|
10
10
|
headings:
|
|
11
|
-
- none: the body is flat; the `## <id>` line and the header fields above the blocks name the unit
|
|
11
|
+
- none: the body is flat; in design/ the `## <id>` line and the header fields above the blocks name the unit, in work/ the frontmatter does
|
|
12
12
|
|
|
13
13
|
frontmatter_fields:
|
|
14
|
-
- none:
|
|
14
|
+
- none: in design/ the header lives above the blocks; in work/ the work item schema holds the frontmatter
|
|
15
15
|
|
|
16
16
|
lexicon_exempt:
|
|
17
17
|
- none: every block is read when the constraint is planned and measured
|
|
@@ -48,5 +48,5 @@ custom_sections:
|
|
|
48
48
|
- none: a block that is not listed above is prose looking for a home
|
|
49
49
|
|
|
50
50
|
enforcement:
|
|
51
|
-
- `ai-factory gcf design check`, over the units in gcf/design/
|
|
51
|
+
- `ai-factory gcf design check`, over the units in gcf/design/ and the items in gcf/work/
|
|
52
52
|
- the gcf pre-commit hook runs the same check and refuses a commit with a blocking finding
|
package/profiles/unit-entity.md
CHANGED
|
@@ -8,10 +8,10 @@ format: cnlp-format.md
|
|
|
8
8
|
mood: declarative
|
|
9
9
|
|
|
10
10
|
headings:
|
|
11
|
-
- none: the body is flat; the `## <id>` line and the header fields above the blocks name the unit
|
|
11
|
+
- none: the body is flat; in design/ the `## <id>` line and the header fields above the blocks name the unit, in work/ the frontmatter does
|
|
12
12
|
|
|
13
13
|
frontmatter_fields:
|
|
14
|
-
- none:
|
|
14
|
+
- none: in design/ the header lives above the blocks; in work/ the work item schema holds the frontmatter
|
|
15
15
|
|
|
16
16
|
lexicon_exempt:
|
|
17
17
|
- none: every block is read when the entity is planned and coded
|
|
@@ -44,5 +44,5 @@ custom_sections:
|
|
|
44
44
|
- none: a block that is not listed above is prose looking for a home
|
|
45
45
|
|
|
46
46
|
enforcement:
|
|
47
|
-
- `ai-factory gcf design check`, over the units in gcf/design/
|
|
47
|
+
- `ai-factory gcf design check`, over the units in gcf/design/ and the items in gcf/work/
|
|
48
48
|
- the gcf pre-commit hook runs the same check and refuses a commit with a blocking finding
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
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
|
|
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 work item in work/<id>.md.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
format: cnlp-format.md
|
|
@@ -8,22 +8,10 @@ format: cnlp-format.md
|
|
|
8
8
|
mood: declarative
|
|
9
9
|
|
|
10
10
|
headings:
|
|
11
|
-
- none: the body is flat; in design/ the `## <id>` line and the header fields above the blocks name the unit, in
|
|
11
|
+
- none: the body is flat; in design/ the `## <id>` line and the header fields above the blocks name the unit, in work/ the frontmatter does
|
|
12
12
|
|
|
13
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
|
|
14
|
+
- none: in design/ the header lives above the blocks; in work/ the work item schema holds the frontmatter
|
|
27
15
|
|
|
28
16
|
lexicon_exempt:
|
|
29
17
|
- none: every block is read when the unit is planned, built or played
|
|
@@ -69,12 +57,12 @@ sections:
|
|
|
69
57
|
- key: contract
|
|
70
58
|
form: bullet-list
|
|
71
59
|
required: yes
|
|
72
|
-
note:
|
|
60
|
+
note: work stage, absent in design/ - paths and entry points, never member lists or values; an architecture rule is a constraint
|
|
73
61
|
|
|
74
62
|
- key: interactions
|
|
75
63
|
form: bullet-list
|
|
76
64
|
required: yes
|
|
77
|
-
note:
|
|
65
|
+
note: work stage, absent in design/ - "<other id>: how the two cross", one line per crossing
|
|
78
66
|
|
|
79
67
|
- key: constraints
|
|
80
68
|
form: bullet-list
|
|
@@ -107,5 +95,5 @@ custom_sections:
|
|
|
107
95
|
- none: a block that is not listed above is prose looking for a home
|
|
108
96
|
|
|
109
97
|
enforcement:
|
|
110
|
-
- `ai-factory gcf design check`, over the units in gcf/design/ and the
|
|
98
|
+
- `ai-factory gcf design check`, over the units in gcf/design/ and the items in gcf/work/
|
|
111
99
|
- the gcf pre-commit hook runs the same check and refuses a commit with a blocking finding
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: unit-resource
|
|
3
|
-
description: The CNL-P profile of a GCF design unit of type resource — an economy resource with its sources and sinks, in design/<system>.md and as a
|
|
3
|
+
description: The CNL-P profile of a GCF design unit of type resource — an economy resource with its sources and sinks, in design/<system>.md and as a work item.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
format: cnlp-format.md
|
|
@@ -8,22 +8,10 @@ format: cnlp-format.md
|
|
|
8
8
|
mood: declarative
|
|
9
9
|
|
|
10
10
|
headings:
|
|
11
|
-
- none: the body is flat; in design/ the `## <id>` line and the header fields above the blocks name the unit, in
|
|
11
|
+
- none: the body is flat; in design/ the `## <id>` line and the header fields above the blocks name the unit, in work/ the frontmatter does
|
|
12
12
|
|
|
13
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
|
|
14
|
+
- none: in design/ the header lives above the blocks; in work/ the work item schema holds the frontmatter
|
|
27
15
|
|
|
28
16
|
lexicon_exempt:
|
|
29
17
|
- none: every block is read when the unit is planned, built or played
|
|
@@ -49,12 +37,12 @@ sections:
|
|
|
49
37
|
- key: contract
|
|
50
38
|
form: bullet-list
|
|
51
39
|
required: yes
|
|
52
|
-
note:
|
|
40
|
+
note: work stage, absent in design/ - paths and entry points, never member lists or values
|
|
53
41
|
|
|
54
42
|
- key: interactions
|
|
55
43
|
form: bullet-list
|
|
56
44
|
required: yes
|
|
57
|
-
note:
|
|
45
|
+
note: work stage, absent in design/ - "<other id>: how the two cross", one line per crossing
|
|
58
46
|
|
|
59
47
|
- key: constraints
|
|
60
48
|
form: bullet-list
|
|
@@ -87,5 +75,5 @@ custom_sections:
|
|
|
87
75
|
- none: a block that is not listed above is prose looking for a home
|
|
88
76
|
|
|
89
77
|
enforcement:
|
|
90
|
-
- `ai-factory gcf design check`, over the units in gcf/design/ and the
|
|
78
|
+
- `ai-factory gcf design check`, over the units in gcf/design/ and the items in gcf/work/
|
|
91
79
|
- the gcf pre-commit hook runs the same check and refuses a commit with a blocking finding
|
package/profiles/unit-rule.md
CHANGED
|
@@ -8,10 +8,10 @@ format: cnlp-format.md
|
|
|
8
8
|
mood: declarative
|
|
9
9
|
|
|
10
10
|
headings:
|
|
11
|
-
- none: the body is flat; the `## <id>` line and the header fields above the blocks name the unit
|
|
11
|
+
- none: the body is flat; in design/ the `## <id>` line and the header fields above the blocks name the unit, in work/ the frontmatter does
|
|
12
12
|
|
|
13
13
|
frontmatter_fields:
|
|
14
|
-
- none:
|
|
14
|
+
- none: in design/ the header lives above the blocks; in work/ the work item schema holds the frontmatter
|
|
15
15
|
|
|
16
16
|
lexicon_exempt:
|
|
17
17
|
- none: every block is read when the rule is planned and tested
|
|
@@ -48,5 +48,5 @@ custom_sections:
|
|
|
48
48
|
- none: a block that is not listed above is prose looking for a home
|
|
49
49
|
|
|
50
50
|
enforcement:
|
|
51
|
-
- `ai-factory gcf design check`, over the units in gcf/design/
|
|
51
|
+
- `ai-factory gcf design check`, over the units in gcf/design/ and the items in gcf/work/
|
|
52
52
|
- the gcf pre-commit hook runs the same check and refuses a commit with a blocking finding
|
package/skills/gcf-card/SKILL.md
CHANGED
|
@@ -23,10 +23,11 @@ forbidden_behaviors:
|
|
|
23
23
|
- do not put `until:` in the frontmatter: it is a line annotation inside slots
|
|
24
24
|
- do not move a section, edit the backlog or write `status:` by hand: `ai-factory gcf take`, `item new`, `done`, `drop` and the item moves own those
|
|
25
25
|
- do not reuse an id: an ended id stays ended; rework gets a new id with `reopens: <old id>`
|
|
26
|
+
- do not remove a D2 record cited as `assumed: q-NNN`: it stays in `gcf/open-questions.md` until its `until:` fires; when the human confirms the value earlier, rewrite each citation to `answer: q-NNN` first
|
|
26
27
|
|
|
27
28
|
outputs:
|
|
28
29
|
- each answered slot gets a provenance record `- slot: <block>` with `answer: manual`, or `answer: q-NNN` when it closes a question
|
|
29
|
-
- a design-unit item body carries every block of its type
|
|
30
|
+
- a design-unit item body carries every block of its type; a mechanic or resource item adds `contract:` (paths and entry points) and `interactions:` (`<other id>: how the two cross`, or `- none: <why>`)
|
|
30
31
|
- every item carries a `live:` delta before it ends: what it adds, modifies or removes in `gcf/LIVE.md`, or `none: <why>`
|
|
31
32
|
|
|
32
33
|
workflow:
|
|
@@ -44,7 +45,7 @@ workflow:
|
|
|
44
45
|
12. write each answer into the unit in place, with its `answer:` provenance record
|
|
45
46
|
13. run `ai-factory gcf design check --json` until the unit's entry in `units` reads `ready: true`, then commit the answered unit
|
|
46
47
|
14. run `ai-factory gcf take <id>` for one unit, or `ai-factory gcf take <id> <id> --feature <feature-id>` to group several under a feature; it writes `gcf/work/<id>.md` and removes the sections and backlog lines in one step
|
|
47
|
-
15. in each
|
|
48
|
+
15. in each mechanic or resource item write `contract:` from the code tree and `interactions:` from the units and items that write the same state; a soft unit whose human wants no playtest gets `gate: auto` - it will close as unplayed
|
|
48
49
|
16. write the item's `live:` delta (a feature's children keep `none: parent`; the feature writes the capability), run `ai-factory gcf design check`, and commit
|
|
49
50
|
17. report the item paths, each item's gate, and for a verdict item the one question the playtest will answer
|
|
50
51
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
## Game Context Framework (from gcf-aif-ext)
|
|
2
|
-
|
|
3
1
|
gcf_context:
|
|
4
2
|
- the game's design and its work live in `gcf/`, and the `ai-factory gcf` commands and the gcf skills change them
|
|
5
3
|
- read `gcf/INDEX.md` before anything else under `gcf/`: it lists every subsystem, design unit and work item
|
|
@@ -16,6 +14,7 @@ gcf_work:
|
|
|
16
14
|
- work that cannot go on: run `ai-factory gcf block <id> --reason "<why or the question>"`
|
|
17
15
|
- in a worktree on branch `work/<id>` change no `gcf/` file but the item's `live:`, commit all, then run `ai-factory gcf verify <id> --mark`: `ai-factory gcf integrate` in the main checkout merges it and records the move
|
|
18
16
|
- a design question only the human can answer becomes a D1 record in `gcf/open-questions.md`
|
|
17
|
+
- a word added to a `not:` of `gcf/glossary.md` leaves `gcf/design/` and every item but an unplayed one in the same commit: `ai-factory gcf design check` names each hit as M4
|
|
19
18
|
- the human plays from `ai-factory gcf playtest session`; each free note from that play goes to `ai-factory gcf inbox add --source playtest "<note>"`
|
|
20
19
|
- a release item carries `version:` and `channel:` (`item new <id> --type release --release <v> --channel <c>`): its end writes the `release:` block of `gcf/LIVE.md`
|
|
21
20
|
- `item new` and `take` refuse a type the current phase does not take: run `ai-factory gcf phase` and bring the refusal to the human
|