@latysh-development/monolith 0.0.0-stage → 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +179 -2
- package/dist/brief/cli.d.ts +30 -0
- package/dist/brief/cli.d.ts.map +1 -0
- package/dist/brief/cli.js +85 -0
- package/dist/brief/collect.d.ts +17 -0
- package/dist/brief/collect.d.ts.map +1 -0
- package/dist/brief/collect.js +342 -0
- package/dist/brief/config.d.ts +40 -0
- package/dist/brief/config.d.ts.map +1 -0
- package/dist/brief/config.js +85 -0
- package/dist/brief/constants.d.ts +12 -0
- package/dist/brief/constants.d.ts.map +1 -0
- package/dist/brief/constants.js +11 -0
- package/dist/brief/docs.d.ts +53 -0
- package/dist/brief/docs.d.ts.map +1 -0
- package/dist/brief/docs.js +229 -0
- package/dist/brief/freshness.d.ts +104 -0
- package/dist/brief/freshness.d.ts.map +1 -0
- package/dist/brief/freshness.js +263 -0
- package/dist/brief/git-signals.d.ts +17 -0
- package/dist/brief/git-signals.d.ts.map +1 -0
- package/dist/brief/git-signals.js +101 -0
- package/dist/brief/index.d.ts +11 -0
- package/dist/brief/index.d.ts.map +1 -0
- package/dist/brief/index.js +5 -0
- package/dist/brief/reconcile.d.ts +75 -0
- package/dist/brief/reconcile.d.ts.map +1 -0
- package/dist/brief/reconcile.js +259 -0
- package/dist/brief/render.d.ts +67 -0
- package/dist/brief/render.d.ts.map +1 -0
- package/dist/brief/render.js +458 -0
- package/dist/brief/sanitize.d.ts +38 -0
- package/dist/brief/sanitize.d.ts.map +1 -0
- package/dist/brief/sanitize.js +48 -0
- package/dist/brief/selectors.d.ts +36 -0
- package/dist/brief/selectors.d.ts.map +1 -0
- package/dist/brief/selectors.js +75 -0
- package/dist/brief/story.d.ts +18 -0
- package/dist/brief/story.d.ts.map +1 -0
- package/dist/brief/story.js +59 -0
- package/dist/brief/types.d.ts +126 -0
- package/dist/brief/types.d.ts.map +1 -0
- package/dist/brief/types.js +11 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +199 -0
- package/dist/cli/init-args.d.ts +16 -0
- package/dist/cli/init-args.d.ts.map +1 -0
- package/dist/cli/init-args.js +24 -0
- package/dist/cli/ui-args.d.ts +23 -0
- package/dist/cli/ui-args.d.ts.map +1 -0
- package/dist/cli/ui-args.js +66 -0
- package/dist/cli/ui.d.ts +40 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +180 -0
- package/dist/coordinatorium/checkpoint.d.ts +44 -0
- package/dist/coordinatorium/checkpoint.d.ts.map +1 -0
- package/dist/coordinatorium/checkpoint.js +124 -0
- package/dist/coordinatorium/cli.d.ts +39 -0
- package/dist/coordinatorium/cli.d.ts.map +1 -0
- package/dist/coordinatorium/cli.js +203 -0
- package/dist/coordinatorium/errors.d.ts +112 -0
- package/dist/coordinatorium/errors.d.ts.map +1 -0
- package/dist/coordinatorium/errors.js +145 -0
- package/dist/coordinatorium/index.d.ts +11 -0
- package/dist/coordinatorium/index.d.ts.map +1 -0
- package/dist/coordinatorium/index.js +5 -0
- package/dist/coordinatorium/lock.d.ts +187 -0
- package/dist/coordinatorium/lock.d.ts.map +1 -0
- package/dist/coordinatorium/lock.js +659 -0
- package/dist/coordinatorium/mission.d.ts +32 -0
- package/dist/coordinatorium/mission.d.ts.map +1 -0
- package/dist/coordinatorium/mission.js +87 -0
- package/dist/coordinatorium/state-keeper.d.ts +115 -0
- package/dist/coordinatorium/state-keeper.d.ts.map +1 -0
- package/dist/coordinatorium/state-keeper.js +396 -0
- package/dist/coordinatorium/types.d.ts +69 -0
- package/dist/coordinatorium/types.d.ts.map +1 -0
- package/dist/coordinatorium/types.js +27 -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/assess.d.ts +105 -0
- package/dist/durable/assess.d.ts.map +1 -0
- package/dist/durable/assess.js +239 -0
- package/dist/durable/canonical.d.ts +25 -0
- package/dist/durable/canonical.d.ts.map +1 -0
- package/dist/durable/canonical.js +78 -0
- package/dist/durable/cli.d.ts +37 -0
- package/dist/durable/cli.d.ts.map +1 -0
- package/dist/durable/cli.js +141 -0
- package/dist/durable/errors.d.ts +192 -0
- package/dist/durable/errors.d.ts.map +1 -0
- package/dist/durable/errors.js +323 -0
- package/dist/durable/fs-store.d.ts +254 -0
- package/dist/durable/fs-store.d.ts.map +1 -0
- package/dist/durable/fs-store.js +1493 -0
- package/dist/durable/index.d.ts +20 -0
- package/dist/durable/index.d.ts.map +1 -0
- package/dist/durable/index.js +13 -0
- package/dist/durable/log.d.ts +33 -0
- package/dist/durable/log.d.ts.map +1 -0
- package/dist/durable/log.js +157 -0
- package/dist/durable/memory-store.d.ts +51 -0
- package/dist/durable/memory-store.d.ts.map +1 -0
- package/dist/durable/memory-store.js +302 -0
- package/dist/durable/projection.d.ts +35 -0
- package/dist/durable/projection.d.ts.map +1 -0
- package/dist/durable/projection.js +328 -0
- package/dist/durable/prune-check.d.ts +34 -0
- package/dist/durable/prune-check.d.ts.map +1 -0
- package/dist/durable/prune-check.js +72 -0
- package/dist/durable/register.d.ts +100 -0
- package/dist/durable/register.d.ts.map +1 -0
- package/dist/durable/register.js +532 -0
- package/dist/durable/retention.d.ts +49 -0
- package/dist/durable/retention.d.ts.map +1 -0
- package/dist/durable/retention.js +59 -0
- package/dist/durable/runtime.d.ts +499 -0
- package/dist/durable/runtime.d.ts.map +1 -0
- package/dist/durable/runtime.js +1288 -0
- package/dist/durable/seams.d.ts +44 -0
- package/dist/durable/seams.d.ts.map +1 -0
- package/dist/durable/seams.js +21 -0
- package/dist/durable/testing.d.ts +11 -0
- package/dist/durable/testing.d.ts.map +1 -0
- package/dist/durable/testing.js +25 -0
- package/dist/durable/types.d.ts +442 -0
- package/dist/durable/types.d.ts.map +1 -0
- package/dist/durable/types.js +75 -0
- package/dist/graph/builder.d.ts +11 -0
- package/dist/graph/builder.d.ts.map +1 -0
- package/dist/graph/builder.js +153 -0
- package/dist/graph/identity.d.ts +37 -0
- package/dist/graph/identity.d.ts.map +1 -0
- package/dist/graph/identity.js +46 -0
- package/dist/graph/index.d.ts +7 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +3 -0
- package/dist/graph/resolver.d.ts +41 -0
- package/dist/graph/resolver.d.ts.map +1 -0
- package/dist/graph/resolver.js +36 -0
- package/dist/graph/types.d.ts +42 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/types.js +1 -0
- package/dist/http/index.d.ts +5 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +2 -0
- package/dist/http/server.d.ts +76 -0
- package/dist/http/server.d.ts.map +1 -0
- package/dist/http/server.js +195 -0
- package/dist/http/ui-assets.d.ts +22 -0
- package/dist/http/ui-assets.d.ts.map +1 -0
- package/dist/http/ui-assets.js +100 -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 +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/init/index.d.ts +12 -0
- package/dist/init/index.d.ts.map +1 -0
- package/dist/init/index.js +90 -0
- package/dist/init/prompts.d.ts +3 -0
- package/dist/init/prompts.d.ts.map +1 -0
- package/dist/init/prompts.js +47 -0
- package/dist/init/scaffold.d.ts +29 -0
- package/dist/init/scaffold.d.ts.map +1 -0
- package/dist/init/scaffold.js +208 -0
- package/dist/init/types.d.ts +14 -0
- package/dist/init/types.d.ts.map +1 -0
- package/dist/init/types.js +1 -0
- 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/readmodel/dashboard.d.ts +40 -0
- package/dist/readmodel/dashboard.d.ts.map +1 -0
- package/dist/readmodel/dashboard.js +236 -0
- package/dist/readmodel/index.d.ts +3 -0
- package/dist/readmodel/index.d.ts.map +1 -0
- package/dist/readmodel/index.js +1 -0
- package/dist/readmodel/types.d.ts +104 -0
- package/dist/readmodel/types.d.ts.map +1 -0
- package/dist/readmodel/types.js +15 -0
- package/dist/root/index.d.ts +186 -0
- package/dist/root/index.d.ts.map +1 -0
- package/dist/root/index.js +430 -0
- package/dist/root/safe-write.d.ts +268 -0
- package/dist/root/safe-write.d.ts.map +1 -0
- package/dist/root/safe-write.js +966 -0
- package/dist/root/write-artifacts.d.ts +20 -0
- package/dist/root/write-artifacts.d.ts.map +1 -0
- package/dist/root/write-artifacts.js +42 -0
- package/dist/scriptorium/compile.d.ts +4 -0
- package/dist/scriptorium/compile.d.ts.map +1 -0
- package/dist/scriptorium/compile.js +185 -0
- package/dist/scriptorium/frontmatter.d.ts +13 -0
- package/dist/scriptorium/frontmatter.d.ts.map +1 -0
- package/dist/scriptorium/frontmatter.js +90 -0
- package/dist/scriptorium/index.d.ts +3 -0
- package/dist/scriptorium/index.d.ts.map +1 -0
- package/dist/scriptorium/index.js +1 -0
- package/dist/scriptorium/markdown.d.ts +31 -0
- package/dist/scriptorium/markdown.d.ts.map +1 -0
- package/dist/scriptorium/markdown.js +88 -0
- package/dist/scriptorium/types.d.ts +78 -0
- package/dist/scriptorium/types.d.ts.map +1 -0
- package/dist/scriptorium/types.js +1 -0
- package/dist/types/enforcement.d.ts +11 -0
- package/dist/types/enforcement.d.ts.map +1 -0
- package/dist/types/enforcement.js +1 -0
- package/dist/types/integrity.d.ts +33 -0
- package/dist/types/integrity.d.ts.map +1 -0
- package/dist/types/integrity.js +1 -0
- package/dist/types/project-definition.d.ts +30 -0
- package/dist/types/project-definition.d.ts.map +1 -0
- package/dist/types/project-definition.js +1 -0
- package/dist/ui/assets/cosmic-glass-bg-Dwx47zPu.webp +0 -0
- package/dist/ui/assets/index-C4NhMaq7.js +28 -0
- package/dist/ui/assets/index-DMZ7ZvZY.css +1 -0
- package/dist/ui/index.html +13 -0
- 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/validate/index.d.ts +6 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +214 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +1 -0
- package/package.json +67 -4
- package/templates/.gitkeep +0 -0
- package/templates/default-instance/instruction/.cursorrules +59 -0
- package/templates/default-instance/instruction/.windsurfrules +59 -0
- package/templates/default-instance/instruction/CLAUDE.md +57 -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/boot/HANDOFF.md +49 -0
- package/templates/default-instance/monolith/context/boot/WORK_SESSION.md +30 -0
- package/templates/default-instance/monolith/context/execution/current-state.md +30 -0
- package/templates/default-instance/monolith/context/maps/dependency-map.md +38 -0
- package/templates/default-instance/monolith/context/maps/project-map.md +85 -0
- package/templates/default-instance/monolith/context/maps/roadmap-index.md +41 -0
- package/templates/default-instance/monolith/context/maps/routing-map.md +151 -0
- package/templates/default-instance/monolith/context/maps/semantic-map.md +75 -0
- package/templates/default-instance/monolith/context/models/model-registry.md +37 -0
- package/templates/default-instance/monolith/context/models/privacy-classification.md +35 -0
- package/templates/default-instance/monolith/context/templates/plan-template.md +56 -0
- package/templates/default-instance/monolith/governance/README.md +29 -0
- package/templates/default-instance/monolith/governance/RULE_SCHEMA.md +58 -0
- package/templates/default-instance/monolith/learning/INDEX.md +48 -0
- package/templates/default-instance/monolith/learning/injection-map.md +49 -0
- package/templates/default-instance/monolith/learning/taxonomy.md +30 -0
- package/templates/default-instance/monolith/memory/errors/README.md +26 -0
- package/templates/default-instance/monolith/memory/errors/SCHEMA.md +47 -0
- package/templates/default-instance/monolith/memory/errors/TAXONOMY.md +27 -0
- package/templates/default-instance/monolith/memory/long-term/architecture-principles.md +31 -0
- package/templates/default-instance/monolith/memory/long-term/decisions.md +36 -0
- package/templates/default-instance/monolith/memory/long-term/execution-history.md +20 -0
- package/templates/default-instance/monolith/memory/long-term/tech-debt.md +25 -0
- package/templates/default-instance/monolith/memory/working/active.md +41 -0
- package/templates/default-instance/monolith/memory/working/governance-result.md +0 -0
- package/templates/default-instance/monolith/memory/working/integrity-result.md +0 -0
- package/templates/default-instance/monolith/memory/working/story-index.md +28 -0
- package/templates/default-instance/monolith/protocols/boot/boot-sequence.md +59 -0
- package/templates/default-instance/monolith/protocols/cognition/cognitive-modes.md +90 -0
- package/templates/default-instance/monolith/protocols/cognition/dry-run.md +56 -0
- package/templates/default-instance/monolith/protocols/cognition/execution-trace-format.md +52 -0
- package/templates/default-instance/monolith/protocols/cognition/semantic-planning.md +51 -0
- package/templates/default-instance/monolith/protocols/execution/hybrid-execution.md +37 -0
- package/templates/default-instance/monolith/protocols/execution/local-llm-governance.md +42 -0
- package/templates/default-instance/monolith/protocols/governance/command-policy.md +51 -0
- package/templates/default-instance/monolith/protocols/governance/core-invariants.md +40 -0
- package/templates/default-instance/monolith/protocols/governance/memory-governance.md +54 -0
- package/templates/default-instance/monolith/protocols/governance/metadata-schema.md +62 -0
- package/templates/default-instance/monolith/protocols/governance/skills.md +53 -0
- package/templates/default-instance/monolith/protocols/governance/work-session.md +46 -0
- package/templates/default-instance/monolith/skills/INDEX.md +32 -0
- package/templates/default-instance/monolith/skills/current-status/SKILL.md +47 -0
- package/templates/default-instance/monolith/skills/finalize-story/SKILL.md +86 -0
- package/templates/default-instance/monolith/skills/global-status/SKILL.md +56 -0
- package/templates/default-instance/monolith/skills/idea-capture/SKILL.md +157 -0
- package/templates/default-instance/monolith/skills/self-improvement/SKILL.md +77 -0
- package/templates/default-instance/monolith/state.json +11 -0
- package/templates/default-instance/monolith/tests/INDEX.md +57 -0
- package/templates/default-instance/monolith/tests/scenarios/architecture-review.md +32 -0
- package/templates/default-instance/monolith/tests/scenarios/debug-failure.md +32 -0
- package/templates/default-instance/monolith/tests/scenarios/dry-run-activation.md +33 -0
- package/templates/default-instance/monolith/tests/scenarios/execution-trace-verification.md +31 -0
- package/templates/default-instance/monolith/tests/scenarios/finalize-story.md +35 -0
- package/templates/default-instance/monolith/tests/scenarios/implement-feature.md +36 -0
- package/templates/default-instance/monolith/tests/scenarios/local-llm-delegation.md +31 -0
- package/templates/default-instance/monolith/tests/scenarios/planning-request.md +32 -0
- package/templates/default-instance/monolith/tests/scenarios/sensitive-context-task.md +30 -0
- package/templates/default-instance/monolith/tests/scenarios/validator-verification.md +33 -0
- package/templates/default-instance/monolith/tests/validation/expected-behavior.md +70 -0
- package/templates/default-instance/monolith/tests/validators/INDEX.md +17 -0
- package/templates/default-instance/monolith/tests/validators/delegation-semantics.md +22 -0
- package/templates/default-instance/monolith/tests/validators/mode-consistency.md +25 -0
- package/templates/default-instance/monolith/tests/validators/privacy-routing.md +23 -0
- package/templates/default-instance/monolith/tests/validators/protocol-references.md +27 -0
- package/templates/default-instance/monolith/tests/validators/routing-integrity.md +30 -0
- package/templates/default-instance/projectroot/AGENTS.md +111 -0
- package/templates/default-instance/projectroot/monolith.config.json +32 -0
- package/templates/default-instance/projectroot/project.definition.mjs.template +41 -0
|
@@ -0,0 +1,194 @@
|
|
|
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 { IDEA_LIMITS, IDEA_STATUSES, IdeaError } from './types.js';
|
|
24
|
+
const BODY_HEADING = '\n## Description\n\n';
|
|
25
|
+
const ID_RE = /^IDEA-(\d{3,9})$/;
|
|
26
|
+
const FILE_RE = /^(IDEA-\d{3,9})\.md$/;
|
|
27
|
+
// C0 controls, DEL and C1 controls (a title is shown in a terminal).
|
|
28
|
+
// eslint-disable-next-line no-control-regex
|
|
29
|
+
const CONTROL_RE = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
|
|
30
|
+
export function isIdeaId(value) {
|
|
31
|
+
return ID_RE.test(value);
|
|
32
|
+
}
|
|
33
|
+
export function ideaNumber(id) {
|
|
34
|
+
const m = ID_RE.exec(id);
|
|
35
|
+
if (!m)
|
|
36
|
+
throw new IdeaError('IDEA_INVALID_ID', `"${id}" is not an idea id (expected IDEA-NNN).`);
|
|
37
|
+
return Number(m[1]);
|
|
38
|
+
}
|
|
39
|
+
export function formatIdeaId(n) {
|
|
40
|
+
return `IDEA-${String(n).padStart(3, '0')}`;
|
|
41
|
+
}
|
|
42
|
+
/** `IDEA-007.md` -> `IDEA-007`; anything else (temp files, other documents) -> null. */
|
|
43
|
+
export function ideaIdFromFileName(name) {
|
|
44
|
+
const m = FILE_RE.exec(name);
|
|
45
|
+
return m ? m[1] : null;
|
|
46
|
+
}
|
|
47
|
+
export function ideaFileName(id) {
|
|
48
|
+
ideaNumber(id);
|
|
49
|
+
return `${id}.md`;
|
|
50
|
+
}
|
|
51
|
+
function invalid(message) {
|
|
52
|
+
return new IdeaError('IDEA_INVALID_INPUT', message);
|
|
53
|
+
}
|
|
54
|
+
const LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
55
|
+
/** An unpaired surrogate would be rewritten to U+FFFD by UTF-8 encoding: not lossless. */
|
|
56
|
+
function wellFormed(s) {
|
|
57
|
+
return !LONE_SURROGATE_RE.test(s);
|
|
58
|
+
}
|
|
59
|
+
/** Validates user input; returns it unchanged (content is never trimmed or rewritten). */
|
|
60
|
+
export function validateNewIdea(input) {
|
|
61
|
+
const { title, description, context } = input;
|
|
62
|
+
if (typeof title !== 'string' || title.trim() === '')
|
|
63
|
+
throw invalid('title must not be empty.');
|
|
64
|
+
if (typeof description !== 'string' || description.trim() === '')
|
|
65
|
+
throw invalid('description must not be empty.');
|
|
66
|
+
if (CONTROL_RE.test(title))
|
|
67
|
+
throw invalid('title must be a single line without control characters.');
|
|
68
|
+
if (title.length > IDEA_LIMITS.title)
|
|
69
|
+
throw invalid(`title is too long (${title.length} > ${IDEA_LIMITS.title} characters).`);
|
|
70
|
+
if (description.length > IDEA_LIMITS.description) {
|
|
71
|
+
throw invalid(`description is too long (${description.length} > ${IDEA_LIMITS.description} characters).`);
|
|
72
|
+
}
|
|
73
|
+
if (context !== undefined) {
|
|
74
|
+
if (typeof context !== 'string' || context.trim() === '')
|
|
75
|
+
throw invalid('context, when given, must not be empty.');
|
|
76
|
+
if (context.length > IDEA_LIMITS.context) {
|
|
77
|
+
throw invalid(`context is too long (${context.length} > ${IDEA_LIMITS.context} characters).`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const [name, v] of [['title', title], ['description', description], ['context', context]]) {
|
|
81
|
+
if (v !== undefined && !wellFormed(v))
|
|
82
|
+
throw invalid(`${name} contains an unpaired surrogate and cannot be stored losslessly.`);
|
|
83
|
+
}
|
|
84
|
+
return input;
|
|
85
|
+
}
|
|
86
|
+
export function serializeIdea(idea) {
|
|
87
|
+
const lines = [
|
|
88
|
+
'---',
|
|
89
|
+
'type: idea',
|
|
90
|
+
`id: ${JSON.stringify(idea.id)}`,
|
|
91
|
+
`title: ${JSON.stringify(idea.title)}`,
|
|
92
|
+
`status: ${idea.status}`,
|
|
93
|
+
`createdAt: ${JSON.stringify(idea.createdAt)}`,
|
|
94
|
+
];
|
|
95
|
+
if (idea.context !== undefined)
|
|
96
|
+
lines.push(`context: ${JSON.stringify(idea.context)}`);
|
|
97
|
+
if (idea.tags !== undefined && idea.tags.length > 0)
|
|
98
|
+
lines.push(`tags: ${JSON.stringify(idea.tags)}`);
|
|
99
|
+
lines.push('---');
|
|
100
|
+
return `${lines.join('\n')}\n${BODY_HEADING}${idea.description}\n`;
|
|
101
|
+
}
|
|
102
|
+
function corrupt(id, reason) {
|
|
103
|
+
return new IdeaError('IDEA_CORRUPT', `${id}: ${reason}`);
|
|
104
|
+
}
|
|
105
|
+
function scalar(raw) {
|
|
106
|
+
const v = raw.trim();
|
|
107
|
+
if (v.startsWith('"')) {
|
|
108
|
+
let parsed;
|
|
109
|
+
try {
|
|
110
|
+
parsed = JSON.parse(v);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
throw new Error('malformed quoted string');
|
|
114
|
+
}
|
|
115
|
+
if (typeof parsed !== 'string')
|
|
116
|
+
throw new Error('malformed quoted string');
|
|
117
|
+
return parsed;
|
|
118
|
+
}
|
|
119
|
+
return v;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Parse an idea file. `expectedId` is the id implied by the file name; a
|
|
123
|
+
* mismatch with the `id:` field is corruption, never silently "fixed".
|
|
124
|
+
*/
|
|
125
|
+
export function parseIdea(expectedId, text) {
|
|
126
|
+
const normalized = text.startsWith('') ? text.slice(1) : text;
|
|
127
|
+
if (!normalized.startsWith('---\n'))
|
|
128
|
+
throw corrupt(expectedId, 'missing frontmatter.');
|
|
129
|
+
const end = normalized.indexOf('\n---\n', 3);
|
|
130
|
+
if (end === -1)
|
|
131
|
+
throw corrupt(expectedId, 'frontmatter is not terminated.');
|
|
132
|
+
const fm = Object.create(null);
|
|
133
|
+
for (const line of normalized.slice(4, end).split('\n')) {
|
|
134
|
+
if (line.trim() === '')
|
|
135
|
+
continue;
|
|
136
|
+
const colon = line.indexOf(':');
|
|
137
|
+
if (colon <= 0)
|
|
138
|
+
throw corrupt(expectedId, `unreadable frontmatter line ${JSON.stringify(line.slice(0, 60))}.`);
|
|
139
|
+
const key = line.slice(0, colon).trim();
|
|
140
|
+
if (key in fm)
|
|
141
|
+
throw corrupt(expectedId, `duplicate frontmatter key "${key}".`);
|
|
142
|
+
fm[key] = line.slice(colon + 1);
|
|
143
|
+
}
|
|
144
|
+
const field = (key) => {
|
|
145
|
+
if (!(key in fm))
|
|
146
|
+
throw corrupt(expectedId, `missing "${key}".`);
|
|
147
|
+
try {
|
|
148
|
+
return scalar(fm[key]);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
throw corrupt(expectedId, `"${key}": ${err.message}.`);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
if ('type' in fm && field('type') !== 'idea')
|
|
155
|
+
throw corrupt(expectedId, `type is "${field('type')}", expected "idea".`);
|
|
156
|
+
const id = field('id');
|
|
157
|
+
if (id !== expectedId)
|
|
158
|
+
throw corrupt(expectedId, `file name says ${expectedId} but the id field says ${JSON.stringify(id)}.`);
|
|
159
|
+
const title = field('title');
|
|
160
|
+
if (title.trim() === '')
|
|
161
|
+
throw corrupt(expectedId, 'title is empty.');
|
|
162
|
+
const status = field('status');
|
|
163
|
+
if (!IDEA_STATUSES.includes(status)) {
|
|
164
|
+
throw corrupt(expectedId, `unknown status ${JSON.stringify(status)} (expected ${IDEA_STATUSES.join(' | ')}).`);
|
|
165
|
+
}
|
|
166
|
+
const createdAt = field('createdAt');
|
|
167
|
+
if (Number.isNaN(Date.parse(createdAt)))
|
|
168
|
+
throw corrupt(expectedId, `createdAt ${JSON.stringify(createdAt)} is not a date.`);
|
|
169
|
+
const body = normalized.slice(end + 5); // right after the closing "---\n"
|
|
170
|
+
if (!body.startsWith(BODY_HEADING))
|
|
171
|
+
throw corrupt(expectedId, 'missing "## Description" section.');
|
|
172
|
+
let description = body.slice(BODY_HEADING.length);
|
|
173
|
+
if (description.endsWith('\n'))
|
|
174
|
+
description = description.slice(0, -1);
|
|
175
|
+
if (description.trim() === '')
|
|
176
|
+
throw corrupt(expectedId, 'description is empty.');
|
|
177
|
+
const idea = { id, title, description, createdAt, status: status };
|
|
178
|
+
if ('context' in fm)
|
|
179
|
+
idea.context = field('context');
|
|
180
|
+
if ('tags' in fm) {
|
|
181
|
+
let tags;
|
|
182
|
+
try {
|
|
183
|
+
tags = JSON.parse(fm.tags);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
throw corrupt(expectedId, '"tags" is not a JSON array of strings.');
|
|
187
|
+
}
|
|
188
|
+
if (!Array.isArray(tags) || !tags.every((t) => typeof t === 'string')) {
|
|
189
|
+
throw corrupt(expectedId, '"tags" is not a JSON array of strings.');
|
|
190
|
+
}
|
|
191
|
+
idea.tags = tags;
|
|
192
|
+
}
|
|
193
|
+
return idea;
|
|
194
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { IDEA_STATUSES, IDEA_LIMITS, IdeaError } from './types.js';
|
|
2
|
+
export type { Idea, IdeaStatus, IdeaEntry, NewIdea, IdeaErrorCode } from './types.js';
|
|
3
|
+
export { ideaStore, addIdea, getIdea, listIdeas } from './store.js';
|
|
4
|
+
export type { IdeaStore, AddIdeaOptions, AddIdeaResult } from './store.js';
|
|
5
|
+
export { serializeIdea, parseIdea, isIdeaId } from './format.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ideas/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACnE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACtF,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACpE,YAAY,EAAE,SAAS,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idea store (IDEA-001A): one Markdown file per idea in `<monolithRoot>/memory/long-term/ideas/`.
|
|
3
|
+
*
|
|
4
|
+
* - Writes go through the safe-write primitive in `src/root/` (PROJECT scope:
|
|
5
|
+
* a symlink anywhere below the project root is refused; the file is created
|
|
6
|
+
* exclusively via `link()`, so an existing file is never overwritten).
|
|
7
|
+
* - Reads go through the unified bounded reader (`readBoundedFile`).
|
|
8
|
+
* - Ids are allocated by exclusive creation, not by trusting a counter: the
|
|
9
|
+
* next number is only a candidate, and a lost race (`WRITE_TARGET_EXISTS`)
|
|
10
|
+
* re-scans and tries again. Two processes can therefore never publish the same id.
|
|
11
|
+
*
|
|
12
|
+
* - Ids are permanently reserved (IDEA-001D): before an idea file is written, an
|
|
13
|
+
* empty-ish marker `ideas/.ids/IDEA-NNN` is created exclusively. The next id is
|
|
14
|
+
* `max(idea files, markers) + 1`, so deleting an idea file never frees its id.
|
|
15
|
+
* A crash between marker and file leaves an orphaned marker: that id is simply
|
|
16
|
+
* burned (a gap), never reused. Existing ideas without a marker (created before
|
|
17
|
+
* IDEA-001D) get one on the next `add`. No shared counter, no lock.
|
|
18
|
+
*
|
|
19
|
+
* Not defended: a local actor swapping directories concurrently (SEC-001B.3,
|
|
20
|
+
* TM-C unsupported). Deleting the `.ids` directory by hand forgets the
|
|
21
|
+
* reservations of ideas that no longer have a file.
|
|
22
|
+
*/
|
|
23
|
+
import { type WriteBoundary, type WriteWarning } from '../root/index.js';
|
|
24
|
+
import type { FsOps } from '../root/index.js';
|
|
25
|
+
import { type Idea, type IdeaEntry, type NewIdea } from './types.js';
|
|
26
|
+
export interface IdeaStore {
|
|
27
|
+
projectRoot: string;
|
|
28
|
+
rootAbs: string;
|
|
29
|
+
dirAbs: string;
|
|
30
|
+
boundary: WriteBoundary;
|
|
31
|
+
}
|
|
32
|
+
/** Resolve (and contain) the ideas directory of the instance rooted at `targetDir`. */
|
|
33
|
+
export declare function ideaStore(targetDir: string): IdeaStore;
|
|
34
|
+
export interface AddIdeaOptions {
|
|
35
|
+
/** Clock seam for tests. */
|
|
36
|
+
now?: () => Date;
|
|
37
|
+
/** Fault-injection seam for tests (forwarded to the safe-write primitive). */
|
|
38
|
+
ops?: Partial<FsOps>;
|
|
39
|
+
/** Receives post-publication findings (the idea IS saved when these fire). */
|
|
40
|
+
onWarning?: (w: WriteWarning) => void;
|
|
41
|
+
}
|
|
42
|
+
export interface AddIdeaResult {
|
|
43
|
+
idea: Idea;
|
|
44
|
+
path: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function addIdea(store: IdeaStore, input: NewIdea, opts?: AddIdeaOptions): AddIdeaResult;
|
|
47
|
+
/** Read one idea. Throws IDEA_INVALID_ID / IDEA_NOT_FOUND / IDEA_UNSAFE_PATH / IDEA_CORRUPT. Never writes. */
|
|
48
|
+
export declare function getIdea(store: IdeaStore, id: string): Idea;
|
|
49
|
+
/**
|
|
50
|
+
* All ideas, ordered by number. A file that cannot be read as an idea appears as
|
|
51
|
+
* a non-ok entry instead of aborting the listing. A symlinked ideas directory
|
|
52
|
+
* is refused as a whole. Never writes.
|
|
53
|
+
*/
|
|
54
|
+
export declare function listIdeas(store: IdeaStore): IdeaEntry[];
|
|
55
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/ideas/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,EAWL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EAA0B,KAAK,IAAI,EAAE,KAAK,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAQ7F,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,aAAa,CAAC;CACzB;AAED,uFAAuF;AACvF,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAetD;AAED,MAAM,WAAW,cAAc;IAC7B,4BAA4B;IAC5B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,8EAA8E;IAC9E,GAAG,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACrB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,IAAI,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AA+DD,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,GAAE,cAAmB,GAAG,aAAa,CA4ElG;AAsBD,8GAA8G;AAC9G,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAM1D;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,EAAE,CAiBvD"}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idea store (IDEA-001A): one Markdown file per idea in `<monolithRoot>/memory/long-term/ideas/`.
|
|
3
|
+
*
|
|
4
|
+
* - Writes go through the safe-write primitive in `src/root/` (PROJECT scope:
|
|
5
|
+
* a symlink anywhere below the project root is refused; the file is created
|
|
6
|
+
* exclusively via `link()`, so an existing file is never overwritten).
|
|
7
|
+
* - Reads go through the unified bounded reader (`readBoundedFile`).
|
|
8
|
+
* - Ids are allocated by exclusive creation, not by trusting a counter: the
|
|
9
|
+
* next number is only a candidate, and a lost race (`WRITE_TARGET_EXISTS`)
|
|
10
|
+
* re-scans and tries again. Two processes can therefore never publish the same id.
|
|
11
|
+
*
|
|
12
|
+
* - Ids are permanently reserved (IDEA-001D): before an idea file is written, an
|
|
13
|
+
* empty-ish marker `ideas/.ids/IDEA-NNN` is created exclusively. The next id is
|
|
14
|
+
* `max(idea files, markers) + 1`, so deleting an idea file never frees its id.
|
|
15
|
+
* A crash between marker and file leaves an orphaned marker: that id is simply
|
|
16
|
+
* burned (a gap), never reused. Existing ideas without a marker (created before
|
|
17
|
+
* IDEA-001D) get one on the next `add`. No shared counter, no lock.
|
|
18
|
+
*
|
|
19
|
+
* Not defended: a local actor swapping directories concurrently (SEC-001B.3,
|
|
20
|
+
* TM-C unsupported). Deleting the `.ids` directory by hand forgets the
|
|
21
|
+
* reservations of ideas that no longer have a file.
|
|
22
|
+
*/
|
|
23
|
+
import { lstatSync, readdirSync } from 'fs';
|
|
24
|
+
import { join } from 'path';
|
|
25
|
+
import { RootContainmentError, SafeWriteError, assertContainedFile, createFile, openParent, projectBoundary, readBoundedFile, resolveMonolithRoot, } from '../root/index.js';
|
|
26
|
+
import { formatIdeaId, ideaFileName, ideaIdFromFileName, ideaNumber, isIdeaId, parseIdea, serializeIdea, validateNewIdea } from './format.js';
|
|
27
|
+
import { IDEA_LIMITS, IdeaError } from './types.js';
|
|
28
|
+
const IDEAS_REL = 'memory/long-term/ideas';
|
|
29
|
+
const IDS_DIR = '.ids';
|
|
30
|
+
const MARKER_RE = /^(IDEA-\d{3,9})$/;
|
|
31
|
+
const MAX_IDEA_NUMBER = 999_999_999;
|
|
32
|
+
const MAX_ALLOCATION_ATTEMPTS = 64;
|
|
33
|
+
/** Resolve (and contain) the ideas directory of the instance rooted at `targetDir`. */
|
|
34
|
+
export function ideaStore(targetDir) {
|
|
35
|
+
let root;
|
|
36
|
+
try {
|
|
37
|
+
root = resolveMonolithRoot(targetDir);
|
|
38
|
+
assertContainedFile(root, IDEAS_REL);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (err instanceof RootContainmentError)
|
|
42
|
+
throw new IdeaError('IDEA_UNSAFE_PATH', err.message, { cause: err });
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
projectRoot: root.projectRoot,
|
|
47
|
+
rootAbs: root.rootAbs,
|
|
48
|
+
dirAbs: join(root.rootAbs, IDEAS_REL),
|
|
49
|
+
boundary: projectBoundary(root.projectRoot),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function unsafe(err) {
|
|
53
|
+
return new IdeaError('IDEA_UNSAFE_PATH', err.message, { cause: err });
|
|
54
|
+
}
|
|
55
|
+
/** Ids of the idea files in the ideas directory, ordered by number. Missing directory -> empty. */
|
|
56
|
+
function scanIds(store) {
|
|
57
|
+
let names;
|
|
58
|
+
try {
|
|
59
|
+
names = readdirSync(store.dirAbs);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if (err.code === 'ENOENT')
|
|
63
|
+
return [];
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
const ids = [];
|
|
67
|
+
for (const name of names) {
|
|
68
|
+
const id = ideaIdFromFileName(name);
|
|
69
|
+
if (id !== null)
|
|
70
|
+
ids.push(id);
|
|
71
|
+
}
|
|
72
|
+
return ids.sort((a, b) => ideaNumber(a) - ideaNumber(b) || (a < b ? -1 : a > b ? 1 : 0));
|
|
73
|
+
}
|
|
74
|
+
/** Ids with a reservation marker in `ideas/.ids/`. Missing directory -> empty. Names that are not `IDEA-NNN` are ignored. */
|
|
75
|
+
function scanReservedIds(store) {
|
|
76
|
+
let names;
|
|
77
|
+
try {
|
|
78
|
+
names = readdirSync(join(store.dirAbs, IDS_DIR));
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
if (err.code === 'ENOENT')
|
|
82
|
+
return [];
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
const ids = [];
|
|
86
|
+
for (const name of names) {
|
|
87
|
+
const m = MARKER_RE.exec(name);
|
|
88
|
+
if (m !== null)
|
|
89
|
+
ids.push(m[1]);
|
|
90
|
+
}
|
|
91
|
+
return ids;
|
|
92
|
+
}
|
|
93
|
+
/** Highest number that is in use or reserved (0 when none). */
|
|
94
|
+
function highestNumber(ids) {
|
|
95
|
+
return ids.reduce((max, id) => Math.max(max, ideaNumber(id)), 0);
|
|
96
|
+
}
|
|
97
|
+
/** Create the marker for `id`. Returns false when it already exists (never overwritten). */
|
|
98
|
+
function reserveId(markers, id, createdAt, opts) {
|
|
99
|
+
try {
|
|
100
|
+
const outcome = createFile(markers, id, `reserved ${createdAt}\n`, {
|
|
101
|
+
mode: 0o666,
|
|
102
|
+
exactMode: false,
|
|
103
|
+
durability: 'full',
|
|
104
|
+
maxBytes: 256,
|
|
105
|
+
ops: opts.ops,
|
|
106
|
+
});
|
|
107
|
+
for (const w of outcome.warnings)
|
|
108
|
+
opts.onWarning?.(w);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
if (err instanceof SafeWriteError && err.code === 'WRITE_TARGET_EXISTS')
|
|
113
|
+
return false;
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function addIdea(store, input, opts = {}) {
|
|
118
|
+
validateNewIdea(input);
|
|
119
|
+
// `add` only creates `memory/long-term/ideas` inside an existing instance; it never invents `.monolith/`.
|
|
120
|
+
let rootStat;
|
|
121
|
+
try {
|
|
122
|
+
rootStat = lstatSync(store.rootAbs);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
rootStat = null;
|
|
126
|
+
}
|
|
127
|
+
if (rootStat === null || !rootStat.isDirectory()) {
|
|
128
|
+
throw new IdeaError('IDEA_NOT_INITIALIZED', `"${store.rootAbs}" is not a MONOLITH instance directory. Run \`monolith init\` first.`);
|
|
129
|
+
}
|
|
130
|
+
let parent;
|
|
131
|
+
try {
|
|
132
|
+
parent = openParent(store.boundary, store.dirAbs, { create: true, ops: opts.ops });
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
if (err instanceof SafeWriteError)
|
|
136
|
+
throw unsafe(err);
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
let markers;
|
|
140
|
+
try {
|
|
141
|
+
markers = openParent(store.boundary, join(store.dirAbs, IDS_DIR), { create: true, ops: opts.ops });
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
if (err instanceof SafeWriteError)
|
|
145
|
+
throw unsafe(err);
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
const createdAt = (opts.now ?? (() => new Date()))().toISOString();
|
|
149
|
+
try {
|
|
150
|
+
// Ideas created before IDEA-001D have no marker: protect them from now on.
|
|
151
|
+
for (const existing of scanIds(store))
|
|
152
|
+
reserveId(markers, existing, createdAt, opts);
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
if (err instanceof SafeWriteError)
|
|
156
|
+
throw unsafe(err);
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
for (let attempt = 0; attempt < MAX_ALLOCATION_ATTEMPTS; attempt++) {
|
|
160
|
+
const next = Math.max(highestNumber(scanIds(store)), highestNumber(scanReservedIds(store))) + 1;
|
|
161
|
+
if (next > MAX_IDEA_NUMBER)
|
|
162
|
+
throw new IdeaError('IDEA_ID_EXHAUSTED', 'the idea id space is exhausted; nothing was written.');
|
|
163
|
+
const id = formatIdeaId(next);
|
|
164
|
+
const idea = { id, title: input.title, description: input.description, createdAt, status: 'inbox' };
|
|
165
|
+
if (input.context !== undefined)
|
|
166
|
+
idea.context = input.context;
|
|
167
|
+
const text = serializeIdea(idea);
|
|
168
|
+
// The character limits do not bound the file (UTF-8 and JSON escaping expand text), and the reader refuses
|
|
169
|
+
// files over `fileBytes`: refuse here, before an id is reserved, so nothing is saved that cannot be read back.
|
|
170
|
+
const bytes = Buffer.byteLength(text, 'utf8');
|
|
171
|
+
if (bytes > IDEA_LIMITS.fileBytes) {
|
|
172
|
+
throw new IdeaError('IDEA_INVALID_INPUT', `the idea is too large once stored (${bytes} > ${IDEA_LIMITS.fileBytes} bytes in UTF-8); shorten the description or context. Nothing was written.`);
|
|
173
|
+
}
|
|
174
|
+
// Reserve first: once the marker exists the id is never handed out again, whatever happens next.
|
|
175
|
+
try {
|
|
176
|
+
if (!reserveId(markers, id, createdAt, opts))
|
|
177
|
+
continue; // lost the race for this number
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
if (err instanceof SafeWriteError)
|
|
181
|
+
throw unsafe(err);
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const outcome = createFile(parent, ideaFileName(id), text, {
|
|
186
|
+
mode: 0o666,
|
|
187
|
+
exactMode: false,
|
|
188
|
+
durability: 'full',
|
|
189
|
+
maxBytes: IDEA_LIMITS.fileBytes,
|
|
190
|
+
ops: opts.ops,
|
|
191
|
+
});
|
|
192
|
+
for (const w of outcome.warnings)
|
|
193
|
+
opts.onWarning?.(w);
|
|
194
|
+
return { idea, path: join(store.dirAbs, ideaFileName(id)) };
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
// Lost the race for this number (or a stray entry holds it): take the next one.
|
|
198
|
+
if (err instanceof SafeWriteError && err.code === 'WRITE_TARGET_EXISTS')
|
|
199
|
+
continue;
|
|
200
|
+
throw err;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
throw new IdeaError('IDEA_ID_EXHAUSTED', `could not allocate an idea id after ${MAX_ALLOCATION_ATTEMPTS} attempts; nothing was written.`);
|
|
204
|
+
}
|
|
205
|
+
function readIdeaFile(store, id) {
|
|
206
|
+
const abs = join(store.dirAbs, ideaFileName(id));
|
|
207
|
+
return readBoundedFile(abs, id, { boundary: store.projectRoot, maxBytes: IDEA_LIMITS.fileBytes }, true);
|
|
208
|
+
}
|
|
209
|
+
function loadIdea(store, id) {
|
|
210
|
+
const read = readIdeaFile(store, id);
|
|
211
|
+
if (!read.ok) {
|
|
212
|
+
if (read.code === 'FILE_MISSING')
|
|
213
|
+
return { missing: true };
|
|
214
|
+
const security = ['FILE_SYMLINK', 'FILE_HARDLINKED', 'FILE_NOT_REGULAR', 'FILE_OUTSIDE_PROJECT', 'READ_BOUNDARY_INVALID', 'READ_UNSUPPORTED_PLATFORM'];
|
|
215
|
+
return { ok: false, id, code: security.includes(read.code) ? 'IDEA_UNSAFE_PATH' : 'IDEA_CORRUPT', reason: `${read.code}: ${read.detail}` };
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
return { ok: true, idea: parseIdea(id, read.content) };
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
if (err instanceof IdeaError)
|
|
222
|
+
return { ok: false, id, code: 'IDEA_CORRUPT', reason: err.message };
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/** Read one idea. Throws IDEA_INVALID_ID / IDEA_NOT_FOUND / IDEA_UNSAFE_PATH / IDEA_CORRUPT. Never writes. */
|
|
227
|
+
export function getIdea(store, id) {
|
|
228
|
+
if (!isIdeaId(id))
|
|
229
|
+
throw new IdeaError('IDEA_INVALID_ID', `"${id}" is not an idea id (expected IDEA-NNN, e.g. IDEA-001).`);
|
|
230
|
+
const loaded = loadIdea(store, id);
|
|
231
|
+
if ('missing' in loaded)
|
|
232
|
+
throw new IdeaError('IDEA_NOT_FOUND', `No idea ${id}.`);
|
|
233
|
+
if (!loaded.ok)
|
|
234
|
+
throw new IdeaError(loaded.code, loaded.reason);
|
|
235
|
+
return loaded.idea;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* All ideas, ordered by number. A file that cannot be read as an idea appears as
|
|
239
|
+
* a non-ok entry instead of aborting the listing. A symlinked ideas directory
|
|
240
|
+
* is refused as a whole. Never writes.
|
|
241
|
+
*/
|
|
242
|
+
export function listIdeas(store) {
|
|
243
|
+
try {
|
|
244
|
+
openParent(store.boundary, store.dirAbs); // every component below the project root: a real directory
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
if (err instanceof SafeWriteError) {
|
|
248
|
+
if (err.code === 'WRITE_PARENT_MISSING')
|
|
249
|
+
return [];
|
|
250
|
+
throw unsafe(err);
|
|
251
|
+
}
|
|
252
|
+
throw err;
|
|
253
|
+
}
|
|
254
|
+
const entries = [];
|
|
255
|
+
for (const id of scanIds(store)) {
|
|
256
|
+
const loaded = loadIdea(store, id);
|
|
257
|
+
if ('missing' in loaded)
|
|
258
|
+
continue; // vanished between readdir and read
|
|
259
|
+
entries.push(loaded);
|
|
260
|
+
}
|
|
261
|
+
return entries;
|
|
262
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manual Idea Capture (IDEA-001A) — types.
|
|
3
|
+
*
|
|
4
|
+
* An idea is a deferred thought saved without leaving the current task. It is
|
|
5
|
+
* NOT a task, a hypothesis or a Durable State record: it does not touch
|
|
6
|
+
* `active.md`, `state.json` or any mission.
|
|
7
|
+
*/
|
|
8
|
+
export declare const IDEA_STATUSES: readonly ["inbox", "planned", "archived"];
|
|
9
|
+
export type IdeaStatus = (typeof IDEA_STATUSES)[number];
|
|
10
|
+
export interface Idea {
|
|
11
|
+
/** `IDEA-NNN` (at least three digits; grows past 999). Never reused while its file exists. */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Single line. */
|
|
14
|
+
title: string;
|
|
15
|
+
/** Free text, stored verbatim (newlines, markdown, CRLF preserved). */
|
|
16
|
+
description: string;
|
|
17
|
+
/** ISO-8601 UTC timestamp, set once at creation. */
|
|
18
|
+
createdAt: string;
|
|
19
|
+
status: IdeaStatus;
|
|
20
|
+
context?: string;
|
|
21
|
+
tags?: string[];
|
|
22
|
+
}
|
|
23
|
+
/** What a caller supplies; `id`, `createdAt` and `status` are assigned by the store. */
|
|
24
|
+
export interface NewIdea {
|
|
25
|
+
title: string;
|
|
26
|
+
description: string;
|
|
27
|
+
context?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Limits are protective ceilings (a CLI argument, not a document store). */
|
|
30
|
+
export declare const IDEA_LIMITS: {
|
|
31
|
+
readonly title: 300;
|
|
32
|
+
readonly description: 100000;
|
|
33
|
+
readonly context: 20000;
|
|
34
|
+
/** Upper bound of one idea file, in bytes (frontmatter + body). */
|
|
35
|
+
readonly fileBytes: number;
|
|
36
|
+
};
|
|
37
|
+
export type IdeaErrorCode = 'IDEA_INVALID_INPUT' | 'IDEA_INVALID_ID' | 'IDEA_NOT_FOUND' | 'IDEA_CORRUPT' | 'IDEA_UNSAFE_PATH' | 'IDEA_NOT_INITIALIZED' | 'IDEA_ID_EXHAUSTED';
|
|
38
|
+
export declare class IdeaError extends Error {
|
|
39
|
+
readonly code: IdeaErrorCode;
|
|
40
|
+
constructor(code: IdeaErrorCode, message: string, options?: {
|
|
41
|
+
cause?: unknown;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** One directory entry of `list`: a readable idea, or a file that could not be read as one. */
|
|
45
|
+
export type IdeaEntry = {
|
|
46
|
+
ok: true;
|
|
47
|
+
idea: Idea;
|
|
48
|
+
} | {
|
|
49
|
+
ok: false;
|
|
50
|
+
id: string;
|
|
51
|
+
code: 'IDEA_CORRUPT' | 'IDEA_UNSAFE_PATH';
|
|
52
|
+
reason: string;
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/ideas/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,eAAO,MAAM,aAAa,2CAA4C,CAAC;AACvE,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAExD,MAAM,WAAW,IAAI;IACnB,8FAA8F;IAC9F,EAAE,EAAE,MAAM,CAAC;IACX,mBAAmB;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,wFAAwF;AACxF,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,6EAA6E;AAC7E,eAAO,MAAM,WAAW;;;;IAItB,mEAAmE;;CAE3D,CAAC;AAEX,MAAM,MAAM,aAAa,GACrB,oBAAoB,GACpB,iBAAiB,GACjB,gBAAgB,GAChB,cAAc,GACd,kBAAkB,GAClB,sBAAsB,GACtB,mBAAmB,CAAC;AAExB,qBAAa,SAAU,SAAQ,KAAK;IAClC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;gBACjB,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAMhF;AAED,+FAA+F;AAC/F,MAAM,MAAM,SAAS,GACjB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,IAAI,CAAA;CAAE,GACxB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,cAAc,GAAG,kBAAkB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manual Idea Capture (IDEA-001A) — types.
|
|
3
|
+
*
|
|
4
|
+
* An idea is a deferred thought saved without leaving the current task. It is
|
|
5
|
+
* NOT a task, a hypothesis or a Durable State record: it does not touch
|
|
6
|
+
* `active.md`, `state.json` or any mission.
|
|
7
|
+
*/
|
|
8
|
+
export const IDEA_STATUSES = ['inbox', 'planned', 'archived'];
|
|
9
|
+
/** Limits are protective ceilings (a CLI argument, not a document store). */
|
|
10
|
+
export const IDEA_LIMITS = {
|
|
11
|
+
title: 300,
|
|
12
|
+
description: 100_000,
|
|
13
|
+
context: 20_000,
|
|
14
|
+
/** Upper bound of one idea file, in bytes (frontmatter + body). */
|
|
15
|
+
fileBytes: 256 * 1024,
|
|
16
|
+
};
|
|
17
|
+
export class IdeaError extends Error {
|
|
18
|
+
code;
|
|
19
|
+
constructor(code, message, options) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'IdeaError';
|
|
22
|
+
this.code = code;
|
|
23
|
+
if (options?.cause !== undefined)
|
|
24
|
+
this.cause = options.cause;
|
|
25
|
+
}
|
|
26
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @latysh-development/monolith — Public API surface
|
|
3
|
+
*
|
|
4
|
+
* This file is the package entrypoint. Exports grow here as Kernel modules
|
|
5
|
+
* are extracted from project instances into this package.
|
|
6
|
+
*
|
|
7
|
+
* Current state: Phase 6 — validate implemented; Scriptorium (Story 1.2) and
|
|
8
|
+
* Explicit Relationship Graph (Story 1.3) integrated.
|
|
9
|
+
* See Phase 7+ roadmap for Kernel extraction.
|
|
10
|
+
*
|
|
11
|
+
* Webway (the frequency-based "meaning linker" prototype from Story 1.1) was
|
|
12
|
+
* extracted into its own repository: https://github.com/Demion-L/webway —
|
|
13
|
+
* it was an independent research track with zero runtime coupling to
|
|
14
|
+
* Scriptorium/Graph, see docs/webway-v0.md for the historical record.
|
|
15
|
+
*/
|
|
16
|
+
export type { ProjectDefinition } from './types/project-definition.js';
|
|
17
|
+
export type { GateCheckResult } from './types/enforcement.js';
|
|
18
|
+
export type { VerificationContext, VerificationReport } from './types/integrity.js';
|
|
19
|
+
export type { ProjectConfig, InitOptions } from './init/types.js';
|
|
20
|
+
export { MONOLITH_VERSION } from './version.js';
|
|
21
|
+
export { runInit } from './init/index.js';
|
|
22
|
+
export type { ScriptoriumInput, ScriptoriumResult, ConceptCandidate, RelationshipCandidate, Evidence, SourceReference, ScriptoriumDiagnostic, ConceptSource, EvidenceSource, HeadingLevel, ObjectDeclaration, DeclaredObjectKind, } from './scriptorium/index.js';
|
|
23
|
+
export { compileMarkdownArtifact, isStructuralHeadingNoise } from './scriptorium/index.js';
|
|
24
|
+
export type { GraphNode, GraphEdge, GraphDiagnostic, ExplicitRelationshipGraph, UnresolvedReference, IdentityConflict, KnowledgeObject, KnowledgeObjectKind, Resolution, UnresolvedResolution, ObjectIndex, } from './graph/index.js';
|
|
25
|
+
export { buildGraph, buildObjectIndex, resolveRef, makeRef, parseRef, isValidIdentifier } from './graph/index.js';
|
|
26
|
+
export type { CoordinatoriumState, Mission, MissionStatus, MissionResult, CoordinatoriumErrorCode, StartMissionInput, FinishMissionInput, StateWriteTarget, StateReadTarget, } from './coordinatorium/index.js';
|
|
27
|
+
export { createInitialState, CoordinatoriumError, StateCorruptedError, StateSchemaError, LockTimeoutError, MissionNotFoundError, InvalidTransitionError, ValidationError, readState, mutateState, projectTarget, trustedDirectoryTarget, startMission, listMissions, getMission, finishMission, } from './coordinatorium/index.js';
|
|
28
|
+
export { SafeWriteError } from './root/index.js';
|
|
29
|
+
export type { WriteErrorCode, WriteWarning, WriteWarningCode, PublicationState } from './root/index.js';
|
|
30
|
+
export * as durable from './durable/index.js';
|
|
31
|
+
export * as ideas from './ideas/index.js';
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,YAAY,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AACvE,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACpF,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAElE,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAG1C,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,QAAQ,EACR,eAAe,EACf,qBAAqB,EACrB,aAAa,EACb,cAAc,EACd,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAG3F,YAAY,EACV,SAAS,EACT,SAAS,EACT,eAAe,EACf,yBAAyB,EACzB,mBAAmB,EACnB,gBAAgB,EAChB,eAAe,EACf,mBAAmB,EACnB,UAAU,EACV,oBAAoB,EACpB,WAAW,GACZ,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAIlH,YAAY,EACV,mBAAmB,EACnB,OAAO,EACP,aAAa,EACb,aAAa,EACb,uBAAuB,EACvB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,GAChB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EACpB,sBAAsB,EACtB,eAAe,EACf,SAAS,EACT,WAAW,EACX,aAAa,EACb,sBAAsB,EACtB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,aAAa,GACd,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAKxG,OAAO,KAAK,OAAO,MAAM,oBAAoB,CAAC;AAG9C,OAAO,KAAK,KAAK,MAAM,kBAAkB,CAAC"}
|