@latysh-development/monolith 0.1.0-alpha.0 → 0.1.0-alpha.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 +63 -16
- package/dist/brief/collect.d.ts.map +1 -1
- package/dist/brief/collect.js +41 -1
- package/dist/brief/constants.d.ts +2 -0
- package/dist/brief/constants.d.ts.map +1 -1
- package/dist/brief/constants.js +2 -0
- package/dist/brief/render.d.ts +7 -0
- package/dist/brief/render.d.ts.map +1 -1
- package/dist/brief/render.js +52 -1
- package/dist/brief/types.d.ts +17 -1
- package/dist/brief/types.d.ts.map +1 -1
- package/dist/cli/index.js +45 -0
- package/dist/discovery/compose.d.ts +28 -0
- package/dist/discovery/compose.d.ts.map +1 -0
- package/dist/discovery/compose.js +159 -0
- package/dist/discovery/contracts.d.ts +211 -0
- package/dist/discovery/contracts.d.ts.map +1 -0
- package/dist/discovery/contracts.js +555 -0
- package/dist/discovery/docs.d.ts +278 -0
- package/dist/discovery/docs.d.ts.map +1 -0
- package/dist/discovery/docs.js +1182 -0
- package/dist/discovery/facts.d.ts +49 -0
- package/dist/discovery/facts.d.ts.map +1 -0
- package/dist/discovery/facts.js +541 -0
- package/dist/discovery/index.d.ts +8 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +9 -0
- package/dist/discovery/orchestrate.d.ts +35 -0
- package/dist/discovery/orchestrate.d.ts.map +1 -0
- package/dist/discovery/orchestrate.js +32 -0
- package/dist/discovery/render.d.ts +36 -0
- package/dist/discovery/render.d.ts.map +1 -0
- package/dist/discovery/render.js +278 -0
- package/dist/discovery/traverse.d.ts +197 -0
- package/dist/discovery/traverse.d.ts.map +1 -0
- package/dist/discovery/traverse.js +510 -0
- package/dist/durable/errors.d.ts +6 -2
- package/dist/durable/errors.d.ts.map +1 -1
- package/dist/durable/errors.js +9 -3
- package/dist/durable/fs-store.d.ts +21 -0
- package/dist/durable/fs-store.d.ts.map +1 -1
- package/dist/durable/fs-store.js +103 -3
- package/dist/durable/prune-check.d.ts +17 -0
- package/dist/durable/prune-check.d.ts.map +1 -1
- package/dist/durable/prune-check.js +27 -0
- package/dist/durable/runtime.d.ts +5 -3
- package/dist/durable/runtime.d.ts.map +1 -1
- package/dist/durable/runtime.js +23 -6
- package/dist/durable/seams.d.ts +4 -0
- package/dist/durable/seams.d.ts.map +1 -1
- package/dist/durable/testing.js +2 -2
- package/dist/finalize/cli.d.ts +10 -0
- package/dist/finalize/cli.d.ts.map +1 -0
- package/dist/finalize/cli.js +134 -0
- package/dist/finalize/core.d.ts +108 -0
- package/dist/finalize/core.d.ts.map +1 -0
- package/dist/finalize/core.js +767 -0
- package/dist/finalize/index.d.ts +5 -0
- package/dist/finalize/index.d.ts.map +1 -0
- package/dist/finalize/index.js +2 -0
- package/dist/finalize/proposal.d.ts +97 -0
- package/dist/finalize/proposal.d.ts.map +1 -0
- package/dist/finalize/proposal.js +213 -0
- package/dist/ideas/cli.d.ts +33 -0
- package/dist/ideas/cli.d.ts.map +1 -0
- package/dist/ideas/cli.js +158 -0
- package/dist/ideas/format.d.ts +38 -0
- package/dist/ideas/format.d.ts.map +1 -0
- package/dist/ideas/format.js +194 -0
- package/dist/ideas/index.d.ts +6 -0
- package/dist/ideas/index.d.ts.map +1 -0
- package/dist/ideas/index.js +3 -0
- package/dist/ideas/store.d.ts +55 -0
- package/dist/ideas/store.d.ts.map +1 -0
- package/dist/ideas/store.js +262 -0
- package/dist/ideas/types.d.ts +54 -0
- package/dist/ideas/types.d.ts.map +1 -0
- package/dist/ideas/types.js +26 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/init/scaffold.d.ts +4 -0
- package/dist/init/scaffold.d.ts.map +1 -1
- package/dist/init/scaffold.js +6 -2
- package/dist/managed/index.d.ts +73 -0
- package/dist/managed/index.d.ts.map +1 -0
- package/dist/managed/index.js +222 -0
- package/dist/root/index.d.ts +7 -0
- package/dist/root/index.d.ts.map +1 -1
- package/dist/root/index.js +13 -1
- package/dist/update/cli.d.ts +8 -0
- package/dist/update/cli.d.ts.map +1 -0
- package/dist/update/cli.js +115 -0
- package/dist/update/index.d.ts +78 -0
- package/dist/update/index.d.ts.map +1 -0
- package/dist/update/index.js +285 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/templates/default-instance/instruction/.cursorrules +6 -0
- package/templates/default-instance/instruction/.windsurfrules +6 -0
- package/templates/default-instance/instruction/CLAUDE.md +4 -0
- package/templates/default-instance/managed/agent.md +20 -0
- package/templates/default-instance/managed/bootstrap.md +7 -0
- package/templates/default-instance/managed/index-idea.md +5 -0
- package/templates/default-instance/managed/routing-idea.md +14 -0
- package/templates/default-instance/monolith/AGENT.md +1 -0
- package/templates/default-instance/monolith/context/maps/routing-map.md +4 -0
- package/templates/default-instance/monolith/context/maps/semantic-map.md +2 -0
- package/templates/default-instance/monolith/memory/working/active.md +11 -0
- package/templates/default-instance/monolith/skills/INDEX.md +4 -0
- package/templates/default-instance/monolith/skills/finalize-story/SKILL.md +51 -1
- package/templates/default-instance/monolith/skills/idea-capture/SKILL.md +157 -0
- package/templates/default-instance/projectroot/AGENTS.md +4 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { parseProposal, validateRelPath, PROPOSAL_SCHEMA_VERSION } from './proposal.js';
|
|
2
|
+
export type { FinalizationProposal, ProposalParse, ExpectedCurrentStory } from './proposal.js';
|
|
3
|
+
export { planFinalization, applyFinalization, readFinalizeConfig, renderCapsule } from './core.js';
|
|
4
|
+
export type { ApplyResult, ApplyStatus, Check, CheckStatus, FinalizationPlan, FinalizeConfig, FinalizeOptions, PlanStatus, PlannedTarget, TargetKind, TargetState, } from './core.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/finalize/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACxF,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACnG,YAAY,EACV,WAAW,EACX,WAAW,EACX,KAAK,EACL,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,UAAU,EACV,aAAa,EACb,UAAU,EACV,WAAW,GACZ,MAAM,WAAW,CAAC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finalization proposal (DOCSYNC-001): the typed, strictly validated input an
|
|
3
|
+
* AI assistant or developer prepares. It carries the *content* of the
|
|
4
|
+
* finalization; MONOLITH decides whether and where it may be applied.
|
|
5
|
+
*
|
|
6
|
+
* Validation is structural and syntactic only. Whether the content is *true*
|
|
7
|
+
* (the summary matches the code) is the owner's judgement, taken when they
|
|
8
|
+
* approve the plan digest — never something this module claims to check.
|
|
9
|
+
*/
|
|
10
|
+
export declare const PROPOSAL_SCHEMA_VERSION = 1;
|
|
11
|
+
/**
|
|
12
|
+
* What the Current Story section of active.md must hold before the transition.
|
|
13
|
+
* There is deliberately no "any": a proposal cannot switch the guard off and
|
|
14
|
+
* silently replace somebody else's active story. `contains` must name the
|
|
15
|
+
* story being finalized (checked in `parseProposal`).
|
|
16
|
+
*/
|
|
17
|
+
export type ExpectedCurrentStory = 'none' | {
|
|
18
|
+
contains: string;
|
|
19
|
+
};
|
|
20
|
+
export interface FinalizationProposal {
|
|
21
|
+
schemaVersion: 1;
|
|
22
|
+
story: {
|
|
23
|
+
/** Story identifier, e.g. `DOCSYNC-001` or `1.3`. Used as the idempotency key. */
|
|
24
|
+
id: string;
|
|
25
|
+
title: string;
|
|
26
|
+
/** Phase directory name under `memory/working/phases/`, e.g. `phase-1`. */
|
|
27
|
+
phase: string;
|
|
28
|
+
/** Text of the `## ` heading in story-index.md the entry belongs to. */
|
|
29
|
+
phaseHeading: string;
|
|
30
|
+
/** Capsule file base name (without `.md`), e.g. `story-docsync-001-finalization`. */
|
|
31
|
+
slug: string;
|
|
32
|
+
/** `YYYY-MM-DD`. Supplied by the caller: MONOLITH never invents a finalization date. */
|
|
33
|
+
date: string;
|
|
34
|
+
};
|
|
35
|
+
/** What the Current Story section of active.md must look like before the transition. */
|
|
36
|
+
expectedCurrentStory: ExpectedCurrentStory;
|
|
37
|
+
work: {
|
|
38
|
+
objective: string;
|
|
39
|
+
implementationSummary: string;
|
|
40
|
+
filesChanged: string[];
|
|
41
|
+
tests: string;
|
|
42
|
+
keyDecisions: string;
|
|
43
|
+
adrs: string[];
|
|
44
|
+
lessons: string;
|
|
45
|
+
};
|
|
46
|
+
/** One-paragraph summary for story-index.md. */
|
|
47
|
+
summary: string;
|
|
48
|
+
keyOutputs: string[];
|
|
49
|
+
history: {
|
|
50
|
+
phaseName: string;
|
|
51
|
+
decision: string;
|
|
52
|
+
build: string;
|
|
53
|
+
};
|
|
54
|
+
/** Complete replacement of HANDOFF.md. Optional: omitted means HANDOFF.md is not touched. */
|
|
55
|
+
handoff?: {
|
|
56
|
+
content: string;
|
|
57
|
+
};
|
|
58
|
+
/** Extra documentation edits; each path must be allowed by `monolith.config.json` `finalization`. */
|
|
59
|
+
docUpdates?: Array<{
|
|
60
|
+
path: string;
|
|
61
|
+
newContent: string;
|
|
62
|
+
reason: string;
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
export type ProposalParse = {
|
|
66
|
+
ok: true;
|
|
67
|
+
proposal: FinalizationProposal;
|
|
68
|
+
} | {
|
|
69
|
+
ok: false;
|
|
70
|
+
errors: string[];
|
|
71
|
+
};
|
|
72
|
+
export declare const LIMITS: {
|
|
73
|
+
readonly idLen: 64;
|
|
74
|
+
readonly titleLen: 200;
|
|
75
|
+
readonly lineLen: 1000;
|
|
76
|
+
readonly blockLen: 20000;
|
|
77
|
+
readonly listItems: 200;
|
|
78
|
+
readonly docUpdates: 20;
|
|
79
|
+
readonly docBytes: number;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* True when `text` holds `id` as a whole Story ID token (case-insensitive).
|
|
83
|
+
* Tokens are maximal runs of the Story ID alphabet [A-Za-z0-9._-]; trailing
|
|
84
|
+
* `.`/`_`/`-` (sentence punctuation) are dropped. So `S-1` is a token of
|
|
85
|
+
* "S-1 in progress" or "(S-1)", but never of "S-10", "XS-1", "1.10" or "11.1".
|
|
86
|
+
*/
|
|
87
|
+
export declare function hasStoryIdToken(text: string, id: string): boolean;
|
|
88
|
+
export declare function validDate(s: string): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Lexical validation of a project-relative document path (R4). Rejects
|
|
91
|
+
* absolute paths, `..`, backslashes, NUL, empty segments and VCS internals.
|
|
92
|
+
* Symlinks are NOT decided here: they are refused by the safe read/write
|
|
93
|
+
* primitives, which look at the real file system.
|
|
94
|
+
*/
|
|
95
|
+
export declare function validateRelPath(p: unknown): string | null;
|
|
96
|
+
export declare function parseProposal(raw: unknown): ProposalParse;
|
|
97
|
+
//# sourceMappingURL=proposal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proposal.d.ts","sourceRoot":"","sources":["../../src/finalize/proposal.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjE,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,CAAC,CAAC;IACjB,KAAK,EAAE;QACL,kFAAkF;QAClF,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,CAAC;QACd,2EAA2E;QAC3E,KAAK,EAAE,MAAM,CAAC;QACd,wEAAwE;QACxE,YAAY,EAAE,MAAM,CAAC;QACrB,qFAAqF;QACrF,IAAI,EAAE,MAAM,CAAC;QACb,wFAAwF;QACxF,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,wFAAwF;IACxF,oBAAoB,EAAE,oBAAoB,CAAC;IAC3C,IAAI,EAAE;QACJ,SAAS,EAAE,MAAM,CAAC;QAClB,qBAAqB,EAAE,MAAM,CAAC;QAC9B,YAAY,EAAE,MAAM,EAAE,CAAC;QACvB,KAAK,EAAE,MAAM,CAAC;QACd,YAAY,EAAE,MAAM,CAAC;QACrB,IAAI,EAAE,MAAM,EAAE,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAChE,6FAA6F;IAC7F,OAAO,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B,qGAAqG;IACrG,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC1E;AAED,MAAM,MAAM,aAAa,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,oBAAoB,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAE3G,eAAO,MAAM,MAAM;;;;;;;;CAQT,CAAC;AAIX;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAGjE;AAaD,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAK5C;AAuBD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAUzD;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CA2GzD"}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finalization proposal (DOCSYNC-001): the typed, strictly validated input an
|
|
3
|
+
* AI assistant or developer prepares. It carries the *content* of the
|
|
4
|
+
* finalization; MONOLITH decides whether and where it may be applied.
|
|
5
|
+
*
|
|
6
|
+
* Validation is structural and syntactic only. Whether the content is *true*
|
|
7
|
+
* (the summary matches the code) is the owner's judgement, taken when they
|
|
8
|
+
* approve the plan digest — never something this module claims to check.
|
|
9
|
+
*/
|
|
10
|
+
export const PROPOSAL_SCHEMA_VERSION = 1;
|
|
11
|
+
export const LIMITS = {
|
|
12
|
+
idLen: 64,
|
|
13
|
+
titleLen: 200,
|
|
14
|
+
lineLen: 1000,
|
|
15
|
+
blockLen: 20000,
|
|
16
|
+
listItems: 200,
|
|
17
|
+
docUpdates: 20,
|
|
18
|
+
docBytes: 1024 * 1024,
|
|
19
|
+
};
|
|
20
|
+
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
21
|
+
/**
|
|
22
|
+
* True when `text` holds `id` as a whole Story ID token (case-insensitive).
|
|
23
|
+
* Tokens are maximal runs of the Story ID alphabet [A-Za-z0-9._-]; trailing
|
|
24
|
+
* `.`/`_`/`-` (sentence punctuation) are dropped. So `S-1` is a token of
|
|
25
|
+
* "S-1 in progress" or "(S-1)", but never of "S-10", "XS-1", "1.10" or "11.1".
|
|
26
|
+
*/
|
|
27
|
+
export function hasStoryIdToken(text, id) {
|
|
28
|
+
const want = id.toLowerCase();
|
|
29
|
+
return (text.match(/[A-Za-z0-9._-]+/g) ?? []).some((t) => t.replace(/[._-]+$/, '').toLowerCase() === want);
|
|
30
|
+
}
|
|
31
|
+
const SLUG_RE = /^[a-z0-9][a-z0-9._-]{0,99}$/;
|
|
32
|
+
const PHASE_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
33
|
+
const DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
34
|
+
function isObject(v) {
|
|
35
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
36
|
+
}
|
|
37
|
+
function noUnknown(obj, allowed, where, errors) {
|
|
38
|
+
for (const k of Object.keys(obj))
|
|
39
|
+
if (!allowed.includes(k))
|
|
40
|
+
errors.push(`${where}: unknown field "${k}".`);
|
|
41
|
+
}
|
|
42
|
+
export function validDate(s) {
|
|
43
|
+
const m = DATE_RE.exec(s);
|
|
44
|
+
if (m === null)
|
|
45
|
+
return false;
|
|
46
|
+
const d = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])));
|
|
47
|
+
return d.getUTCFullYear() === Number(m[1]) && d.getUTCMonth() === Number(m[2]) - 1 && d.getUTCDate() === Number(m[3]);
|
|
48
|
+
}
|
|
49
|
+
function text(v, where, errors, opts) {
|
|
50
|
+
if (typeof v !== 'string') {
|
|
51
|
+
errors.push(`${where}: must be a string.`);
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
if (v.includes('\0'))
|
|
55
|
+
errors.push(`${where}: must not contain NUL.`);
|
|
56
|
+
if (!opts.allowEmpty && v.trim().length === 0)
|
|
57
|
+
errors.push(`${where}: must not be empty.`);
|
|
58
|
+
if (v.length > opts.max)
|
|
59
|
+
errors.push(`${where}: longer than ${opts.max} characters.`);
|
|
60
|
+
if (!opts.multiline && /[\r\n\u2028\u2029]/.test(v))
|
|
61
|
+
errors.push(`${where}: must be a single line.`);
|
|
62
|
+
return v;
|
|
63
|
+
}
|
|
64
|
+
function lines(v, where, errors) {
|
|
65
|
+
if (!Array.isArray(v)) {
|
|
66
|
+
errors.push(`${where}: must be an array of strings.`);
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
if (v.length > LIMITS.listItems)
|
|
70
|
+
errors.push(`${where}: more than ${LIMITS.listItems} items.`);
|
|
71
|
+
return v.map((x, i) => text(x, `${where}[${i}]`, errors, { multiline: false, max: LIMITS.lineLen }));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Lexical validation of a project-relative document path (R4). Rejects
|
|
75
|
+
* absolute paths, `..`, backslashes, NUL, empty segments and VCS internals.
|
|
76
|
+
* Symlinks are NOT decided here: they are refused by the safe read/write
|
|
77
|
+
* primitives, which look at the real file system.
|
|
78
|
+
*/
|
|
79
|
+
export function validateRelPath(p) {
|
|
80
|
+
if (typeof p !== 'string' || p.length === 0 || p.length > 260)
|
|
81
|
+
return 'path must be a non-empty string of at most 260 characters';
|
|
82
|
+
if (p.includes('\0') || p.includes('\\'))
|
|
83
|
+
return 'path must not contain NUL or backslashes';
|
|
84
|
+
if (p.startsWith('/') || /^[A-Za-z]:/.test(p))
|
|
85
|
+
return 'path must be relative to the project root';
|
|
86
|
+
const segs = p.split('/');
|
|
87
|
+
if (segs.some((s) => s === '' || s === '.' || s === '..'))
|
|
88
|
+
return 'path must not contain empty, "." or ".." segments';
|
|
89
|
+
// `.git` at ANY depth (nested repositories, submodule checkouts), any case
|
|
90
|
+
// (case-insensitive file systems), and trailing dot/space (Windows aliases).
|
|
91
|
+
if (segs.some((s) => /^\.git[. ]*$/i.test(s)) || segs.includes('node_modules'))
|
|
92
|
+
return 'path must not contain a .git (any case, any depth) or node_modules component';
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
export function parseProposal(raw) {
|
|
96
|
+
const errors = [];
|
|
97
|
+
if (!isObject(raw))
|
|
98
|
+
return { ok: false, errors: ['proposal must be a JSON object.'] };
|
|
99
|
+
noUnknown(raw, ['schemaVersion', 'story', 'expectedCurrentStory', 'work', 'summary', 'keyOutputs', 'history', 'handoff', 'docUpdates'], 'proposal', errors);
|
|
100
|
+
if (raw.schemaVersion !== PROPOSAL_SCHEMA_VERSION)
|
|
101
|
+
errors.push(`schemaVersion: must be ${PROPOSAL_SCHEMA_VERSION}.`);
|
|
102
|
+
const story = isObject(raw.story) ? raw.story : (errors.push('story: must be an object.'), {});
|
|
103
|
+
noUnknown(story, ['id', 'title', 'phase', 'phaseHeading', 'slug', 'date'], 'story', errors);
|
|
104
|
+
const id = text(story.id, 'story.id', errors, { multiline: false, max: LIMITS.idLen });
|
|
105
|
+
if (id && !ID_RE.test(id))
|
|
106
|
+
errors.push('story.id: must match [A-Za-z0-9][A-Za-z0-9._-]{0,63}.');
|
|
107
|
+
const title = text(story.title, 'story.title', errors, { multiline: false, max: LIMITS.titleLen });
|
|
108
|
+
const phase = text(story.phase, 'story.phase', errors, { multiline: false, max: 32 });
|
|
109
|
+
if (phase && !PHASE_RE.test(phase))
|
|
110
|
+
errors.push('story.phase: must match [a-z0-9][a-z0-9-]{0,31}.');
|
|
111
|
+
const phaseHeading = text(story.phaseHeading, 'story.phaseHeading', errors, { multiline: false, max: LIMITS.titleLen });
|
|
112
|
+
if (phaseHeading.startsWith('#'))
|
|
113
|
+
errors.push('story.phaseHeading: give the heading text without leading "#".');
|
|
114
|
+
const slug = text(story.slug, 'story.slug', errors, { multiline: false, max: 100 });
|
|
115
|
+
if (slug && !SLUG_RE.test(slug))
|
|
116
|
+
errors.push('story.slug: must match [a-z0-9][a-z0-9._-]{0,99}.');
|
|
117
|
+
const date = text(story.date, 'story.date', errors, { multiline: false, max: 10 });
|
|
118
|
+
if (date && !validDate(date))
|
|
119
|
+
errors.push('story.date: must be a real date YYYY-MM-DD.');
|
|
120
|
+
let expected = 'none';
|
|
121
|
+
const e = raw.expectedCurrentStory;
|
|
122
|
+
if (e === 'none')
|
|
123
|
+
expected = e;
|
|
124
|
+
else if (isObject(e) && typeof e.contains === 'string' && e.contains.trim() !== '' && Object.keys(e).length === 1 && e.contains.length <= LIMITS.lineLen) {
|
|
125
|
+
expected = { contains: e.contains };
|
|
126
|
+
if (id && !hasStoryIdToken(e.contains, id)) {
|
|
127
|
+
errors.push(`expectedCurrentStory.contains: must contain the story id "${id}" as a whole token (not as part of a longer id), so the guard cannot match another story.`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else if (e === 'any') {
|
|
131
|
+
errors.push('expectedCurrentStory: "any" is not accepted (it would allow replacing an unrelated active story). Use "none" or {"contains": "<text containing the story id>"}.');
|
|
132
|
+
}
|
|
133
|
+
else
|
|
134
|
+
errors.push('expectedCurrentStory: must be "none" or {"contains": "<text containing the story id>"}.');
|
|
135
|
+
const w = isObject(raw.work) ? raw.work : (errors.push('work: must be an object.'), {});
|
|
136
|
+
noUnknown(w, ['objective', 'implementationSummary', 'filesChanged', 'tests', 'keyDecisions', 'adrs', 'lessons'], 'work', errors);
|
|
137
|
+
const block = (v, name) => text(v, `work.${name}`, errors, { multiline: true, max: LIMITS.blockLen });
|
|
138
|
+
const work = {
|
|
139
|
+
objective: block(w.objective, 'objective'),
|
|
140
|
+
implementationSummary: block(w.implementationSummary, 'implementationSummary'),
|
|
141
|
+
filesChanged: lines(w.filesChanged, 'work.filesChanged', errors),
|
|
142
|
+
tests: block(w.tests, 'tests'),
|
|
143
|
+
keyDecisions: block(w.keyDecisions, 'keyDecisions'),
|
|
144
|
+
adrs: lines(w.adrs, 'work.adrs', errors),
|
|
145
|
+
lessons: block(w.lessons, 'lessons'),
|
|
146
|
+
};
|
|
147
|
+
const summary = text(raw.summary, 'summary', errors, { multiline: false, max: LIMITS.blockLen });
|
|
148
|
+
const keyOutputs = lines(raw.keyOutputs, 'keyOutputs', errors);
|
|
149
|
+
const h = isObject(raw.history) ? raw.history : (errors.push('history: must be an object.'), {});
|
|
150
|
+
noUnknown(h, ['phaseName', 'decision', 'build'], 'history', errors);
|
|
151
|
+
const history = {
|
|
152
|
+
phaseName: text(h.phaseName, 'history.phaseName', errors, { multiline: false, max: LIMITS.titleLen }),
|
|
153
|
+
decision: text(h.decision, 'history.decision', errors, { multiline: false, max: LIMITS.blockLen }),
|
|
154
|
+
build: text(h.build, 'history.build', errors, { multiline: false, max: LIMITS.lineLen }),
|
|
155
|
+
};
|
|
156
|
+
let handoff;
|
|
157
|
+
if (raw.handoff !== undefined) {
|
|
158
|
+
if (!isObject(raw.handoff))
|
|
159
|
+
errors.push('handoff: must be an object {"content": "..."}.');
|
|
160
|
+
else {
|
|
161
|
+
noUnknown(raw.handoff, ['content'], 'handoff', errors);
|
|
162
|
+
const content = text(raw.handoff.content, 'handoff.content', errors, { multiline: true, max: LIMITS.docBytes });
|
|
163
|
+
handoff = { content };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
let docUpdates;
|
|
167
|
+
if (raw.docUpdates !== undefined) {
|
|
168
|
+
if (!Array.isArray(raw.docUpdates))
|
|
169
|
+
errors.push('docUpdates: must be an array.');
|
|
170
|
+
else {
|
|
171
|
+
if (raw.docUpdates.length > LIMITS.docUpdates)
|
|
172
|
+
errors.push(`docUpdates: more than ${LIMITS.docUpdates} entries.`);
|
|
173
|
+
docUpdates = [];
|
|
174
|
+
const seen = new Set();
|
|
175
|
+
raw.docUpdates.forEach((d, i) => {
|
|
176
|
+
const where = `docUpdates[${i}]`;
|
|
177
|
+
if (!isObject(d)) {
|
|
178
|
+
errors.push(`${where}: must be an object.`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
noUnknown(d, ['path', 'newContent', 'reason'], where, errors);
|
|
182
|
+
const bad = validateRelPath(d.path);
|
|
183
|
+
if (bad !== null)
|
|
184
|
+
errors.push(`${where}.path: ${bad}.`);
|
|
185
|
+
const path = typeof d.path === 'string' ? d.path : '';
|
|
186
|
+
if (seen.has(path))
|
|
187
|
+
errors.push(`${where}.path: "${path}" is listed twice.`);
|
|
188
|
+
seen.add(path);
|
|
189
|
+
docUpdates.push({
|
|
190
|
+
path,
|
|
191
|
+
newContent: text(d.newContent, `${where}.newContent`, errors, { multiline: true, max: LIMITS.docBytes }),
|
|
192
|
+
reason: text(d.reason, `${where}.reason`, errors, { multiline: false, max: LIMITS.lineLen }),
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (errors.length > 0)
|
|
198
|
+
return { ok: false, errors };
|
|
199
|
+
const proposal = {
|
|
200
|
+
schemaVersion: 1,
|
|
201
|
+
story: { id, title, phase, phaseHeading, slug, date },
|
|
202
|
+
expectedCurrentStory: expected,
|
|
203
|
+
work,
|
|
204
|
+
summary,
|
|
205
|
+
keyOutputs,
|
|
206
|
+
history,
|
|
207
|
+
};
|
|
208
|
+
if (handoff)
|
|
209
|
+
proposal.handoff = handoff;
|
|
210
|
+
if (docUpdates && docUpdates.length > 0)
|
|
211
|
+
proposal.docUpdates = docUpdates;
|
|
212
|
+
return { ok: true, proposal };
|
|
213
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `monolith idea ...` command handlers (IDEA-001A).
|
|
3
|
+
*
|
|
4
|
+
* Thin layer: strict flag parsing, store calls, output. No LLM, no network;
|
|
5
|
+
* `add` writes exactly one new file under `<monolithRoot>/memory/long-term/ideas/` and
|
|
6
|
+
* touches no other state (not `active.md`, not `state.json`); `list` and `show` write nothing.
|
|
7
|
+
*/
|
|
8
|
+
import type { Readable } from 'stream';
|
|
9
|
+
interface AddFlags {
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
context?: string;
|
|
13
|
+
descriptionStdin?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Strict: every flag needs a value, none may repeat, unknown flags and stray arguments are errors. */
|
|
16
|
+
export declare function parseAddArgs(args: string[]): AddFlags;
|
|
17
|
+
/**
|
|
18
|
+
* Read all of `input` as UTF-8, byte for byte (no trimming, BOM kept). Refuses
|
|
19
|
+
* more than STDIN_MAX_BYTES, invalid UTF-8 and a read error; nothing is
|
|
20
|
+
* interpreted. A terminal is refused instead of waiting for typed input.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readStdinText(input?: Readable & {
|
|
23
|
+
isTTY?: boolean;
|
|
24
|
+
}): Promise<string>;
|
|
25
|
+
export declare function runIdeaAdd(args: string[], targetDir?: string, stdin?: Readable & {
|
|
26
|
+
isTTY?: boolean;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
export declare function runIdeaList(args: string[], targetDir?: string): Promise<void>;
|
|
29
|
+
export declare function runIdeaShow(args: string[], targetDir?: string): Promise<void>;
|
|
30
|
+
/** Dispatcher used by the CLI entrypoint. */
|
|
31
|
+
export declare function runIdeaCommand(args: string[], targetDir?: string): Promise<void>;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/ideas/cli.ts"],"names":[],"mappings":"AACA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAiBvC,UAAU,QAAQ;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAMD,uGAAuG;AACvG,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,CA4BrD;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,KAAK,GAAE,QAAQ,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAyB1G;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,EAAE,KAAK,GAAE,QAAQ,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhJ;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB1F;AAiBD,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAG1F;AAED,6CAA6C;AAC7C,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAe7F"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/**
|
|
3
|
+
* `monolith idea ...` command handlers (IDEA-001A).
|
|
4
|
+
*
|
|
5
|
+
* Thin layer: strict flag parsing, store calls, output. No LLM, no network;
|
|
6
|
+
* `add` writes exactly one new file under `<monolithRoot>/memory/long-term/ideas/` and
|
|
7
|
+
* touches no other state (not `active.md`, not `state.json`); `list` and `show` write nothing.
|
|
8
|
+
*/
|
|
9
|
+
import { reportWriteWarning } from '../root/index.js';
|
|
10
|
+
import { addIdea, getIdea, ideaStore, listIdeas } from './store.js';
|
|
11
|
+
import { IDEA_LIMITS, IdeaError } from './types.js';
|
|
12
|
+
const ADD_USAGE = 'Usage: monolith idea add --title "..." (--description "..." | --description-stdin) [--context "..."]';
|
|
13
|
+
/** UTF-8 needs at most 4 bytes per UTF-16 code unit's worth of text; anything larger cannot fit the description limit. */
|
|
14
|
+
const STDIN_MAX_BYTES = IDEA_LIMITS.description * 4;
|
|
15
|
+
const LIST_USAGE = 'Usage: monolith idea list';
|
|
16
|
+
const SHOW_USAGE = 'Usage: monolith idea show <id>';
|
|
17
|
+
/** Escape control characters so project-controlled text cannot forge terminal output. */
|
|
18
|
+
function printable(s) {
|
|
19
|
+
// eslint-disable-next-line no-control-regex
|
|
20
|
+
return s.replace(/[\u0000-\u0009\u000b-\u001f\u007f-\u009f\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
21
|
+
}
|
|
22
|
+
const ADD_FLAGS = ['--title', '--description', '--context'];
|
|
23
|
+
const STDIN_FLAG = '--description-stdin';
|
|
24
|
+
const KNOWN_FLAGS = [...ADD_FLAGS, STDIN_FLAG];
|
|
25
|
+
/** Strict: every flag needs a value, none may repeat, unknown flags and stray arguments are errors. */
|
|
26
|
+
export function parseAddArgs(args) {
|
|
27
|
+
const out = {};
|
|
28
|
+
for (let i = 0; i < args.length; i++) {
|
|
29
|
+
const flag = args[i];
|
|
30
|
+
if (!KNOWN_FLAGS.includes(flag)) {
|
|
31
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `Unexpected argument ${JSON.stringify(printable(flag))}.\n${ADD_USAGE}`);
|
|
32
|
+
}
|
|
33
|
+
if (flag === STDIN_FLAG) {
|
|
34
|
+
if (out.descriptionStdin)
|
|
35
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `${flag} was given more than once.\n${ADD_USAGE}`);
|
|
36
|
+
out.descriptionStdin = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const key = flag.slice(2);
|
|
40
|
+
if (key in out)
|
|
41
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `${flag} was given more than once.\n${ADD_USAGE}`);
|
|
42
|
+
const value = args[i + 1];
|
|
43
|
+
if (value === undefined || KNOWN_FLAGS.includes(value)) {
|
|
44
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `${flag} needs a value.\n${ADD_USAGE}`);
|
|
45
|
+
}
|
|
46
|
+
out[key] = value;
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
if (out.descriptionStdin && out.description !== undefined) {
|
|
50
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `--description and ${STDIN_FLAG} cannot be combined.\n${ADD_USAGE}`);
|
|
51
|
+
}
|
|
52
|
+
if (out.title === undefined || (out.description === undefined && !out.descriptionStdin)) {
|
|
53
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `--title and --description (or ${STDIN_FLAG}) are required.\n${ADD_USAGE}`);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Read all of `input` as UTF-8, byte for byte (no trimming, BOM kept). Refuses
|
|
59
|
+
* more than STDIN_MAX_BYTES, invalid UTF-8 and a read error; nothing is
|
|
60
|
+
* interpreted. A terminal is refused instead of waiting for typed input.
|
|
61
|
+
*/
|
|
62
|
+
export async function readStdinText(input = process.stdin) {
|
|
63
|
+
if (input.isTTY) {
|
|
64
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `${STDIN_FLAG} needs piped input, but stdin is a terminal.\n${ADD_USAGE}`);
|
|
65
|
+
}
|
|
66
|
+
const chunks = [];
|
|
67
|
+
let total = 0;
|
|
68
|
+
try {
|
|
69
|
+
for await (const chunk of input) {
|
|
70
|
+
const buf = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk;
|
|
71
|
+
total += buf.length;
|
|
72
|
+
if (total > STDIN_MAX_BYTES) {
|
|
73
|
+
input.destroy();
|
|
74
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `stdin is too large (more than ${STDIN_MAX_BYTES} bytes); nothing was written.`);
|
|
75
|
+
}
|
|
76
|
+
chunks.push(buf);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
if (err instanceof IdeaError)
|
|
81
|
+
throw err;
|
|
82
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `could not read stdin: ${err.message}; nothing was written.`, { cause: err });
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(Buffer.concat(chunks));
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
throw new IdeaError('IDEA_INVALID_INPUT', 'stdin is not valid UTF-8; nothing was written.', { cause: err });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export async function runIdeaAdd(args, targetDir = process.cwd(), stdin = process.stdin) {
|
|
92
|
+
const flags = parseAddArgs(args);
|
|
93
|
+
const description = flags.descriptionStdin ? await readStdinText(stdin) : flags.description;
|
|
94
|
+
const store = ideaStore(targetDir);
|
|
95
|
+
const { idea, path } = addIdea(store, { title: flags.title, description, context: flags.context }, { onWarning: (w) => reportWriteWarning(w) });
|
|
96
|
+
console.log(`Saved ${idea.id} [${idea.status}]: ${printable(idea.title)}`);
|
|
97
|
+
console.log(` file: ${path}`);
|
|
98
|
+
console.log('Current task and focus are unchanged. Recall with `monolith idea list` / `monolith idea show ' + idea.id + '`.');
|
|
99
|
+
}
|
|
100
|
+
export async function runIdeaList(args, targetDir = process.cwd()) {
|
|
101
|
+
if (args.length > 0)
|
|
102
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `Unexpected argument ${JSON.stringify(printable(args[0]))}.\n${LIST_USAGE}`);
|
|
103
|
+
const entries = listIdeas(ideaStore(targetDir));
|
|
104
|
+
if (entries.length === 0) {
|
|
105
|
+
console.log('No ideas. Capture one with `monolith idea add --title "..." --description "..."`.');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
console.log(`Ideas (${entries.length}):`);
|
|
109
|
+
let unreadable = 0;
|
|
110
|
+
for (const e of entries) {
|
|
111
|
+
if (e.ok) {
|
|
112
|
+
console.log(` ${e.idea.id} [${e.idea.status}] ${printable(e.idea.title)}`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
unreadable++;
|
|
116
|
+
console.log(` ${e.id} [unreadable] ${printable(e.reason)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (unreadable > 0) {
|
|
120
|
+
console.error(`${unreadable} idea file(s) could not be read; they were left untouched. Inspect them with \`monolith idea show <id>\`.`);
|
|
121
|
+
process.exitCode = 2;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function formatIdea(i) {
|
|
125
|
+
const lines = [
|
|
126
|
+
` id: ${i.id}`,
|
|
127
|
+
` title: ${printable(i.title)}`,
|
|
128
|
+
` status: ${i.status}`,
|
|
129
|
+
` createdAt: ${i.createdAt}`,
|
|
130
|
+
];
|
|
131
|
+
if (i.tags !== undefined && i.tags.length > 0)
|
|
132
|
+
lines.push(` tags: ${i.tags.map(printable).join(', ')}`);
|
|
133
|
+
lines.push(' context: ' + (i.context === undefined ? '(none)' : ''));
|
|
134
|
+
if (i.context !== undefined)
|
|
135
|
+
lines.push(...i.context.split('\n').map((l) => ` ${l}`));
|
|
136
|
+
lines.push(' description:');
|
|
137
|
+
lines.push(...i.description.split('\n').map((l) => ` ${l}`));
|
|
138
|
+
return lines.join('\n');
|
|
139
|
+
}
|
|
140
|
+
export async function runIdeaShow(args, targetDir = process.cwd()) {
|
|
141
|
+
if (args.length !== 1)
|
|
142
|
+
throw new IdeaError('IDEA_INVALID_INPUT', SHOW_USAGE);
|
|
143
|
+
console.log(formatIdea(getIdea(ideaStore(targetDir), args[0])));
|
|
144
|
+
}
|
|
145
|
+
/** Dispatcher used by the CLI entrypoint. */
|
|
146
|
+
export async function runIdeaCommand(args, targetDir = process.cwd()) {
|
|
147
|
+
const [sub, ...rest] = args;
|
|
148
|
+
switch (sub) {
|
|
149
|
+
case 'add':
|
|
150
|
+
return runIdeaAdd(rest, targetDir);
|
|
151
|
+
case 'list':
|
|
152
|
+
return runIdeaList(rest, targetDir);
|
|
153
|
+
case 'show':
|
|
154
|
+
return runIdeaShow(rest, targetDir);
|
|
155
|
+
default:
|
|
156
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `Unknown idea subcommand: ${sub === undefined ? '(none)' : JSON.stringify(printable(sub))}\n${ADD_USAGE}\n ${LIST_USAGE.slice(7)}\n ${SHOW_USAGE.slice(7)}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idea file format: Markdown with a small frontmatter block.
|
|
3
|
+
*
|
|
4
|
+
* ---
|
|
5
|
+
* type: idea
|
|
6
|
+
* id: "IDEA-001"
|
|
7
|
+
* title: "..." (JSON string: any character survives a round trip)
|
|
8
|
+
* status: inbox
|
|
9
|
+
* createdAt: "2026-..."
|
|
10
|
+
* context: "..." (optional)
|
|
11
|
+
* tags: ["a", "b"] (optional)
|
|
12
|
+
* ---
|
|
13
|
+
*
|
|
14
|
+
* ## Description
|
|
15
|
+
*
|
|
16
|
+
* <description, verbatim>
|
|
17
|
+
*
|
|
18
|
+
* Strings are written as JSON strings, which are also valid YAML double-quoted
|
|
19
|
+
* scalars, so ERG/Scriptorium read the file too. The reader additionally accepts
|
|
20
|
+
* a plain unquoted value, because a person edits these files by hand.
|
|
21
|
+
* The description is the body: no escaping, no trimming.
|
|
22
|
+
*/
|
|
23
|
+
import { type Idea, type NewIdea } from './types.js';
|
|
24
|
+
export declare function isIdeaId(value: string): boolean;
|
|
25
|
+
export declare function ideaNumber(id: string): number;
|
|
26
|
+
export declare function formatIdeaId(n: number): string;
|
|
27
|
+
/** `IDEA-007.md` -> `IDEA-007`; anything else (temp files, other documents) -> null. */
|
|
28
|
+
export declare function ideaIdFromFileName(name: string): string | null;
|
|
29
|
+
export declare function ideaFileName(id: string): string;
|
|
30
|
+
/** Validates user input; returns it unchanged (content is never trimmed or rewritten). */
|
|
31
|
+
export declare function validateNewIdea(input: NewIdea): NewIdea;
|
|
32
|
+
export declare function serializeIdea(idea: Idea): string;
|
|
33
|
+
/**
|
|
34
|
+
* Parse an idea file. `expectedId` is the id implied by the file name; a
|
|
35
|
+
* mismatch with the `id:` field is corruption, never silently "fixed".
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseIdea(expectedId: string, text: string): Idea;
|
|
38
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/ideas/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAyC,KAAK,IAAI,EAAmB,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAS7G,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAI7C;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,wFAAwF;AACxF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG9D;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAG/C;AAaD,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAmBvD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAahD;AAqBD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CA0DhE"}
|