@danypops/papyrus 0.51.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/package.json +1 -1
- package/src/cli/projects-command.ts +105 -0
- package/src/cli.ts +7 -0
- package/src/domain/project-registry.ts +25 -0
- package/src/handlers/projects.ts +45 -0
- package/src/handlers/registry.ts +2 -0
- package/src/modules/projects.ts +48 -0
- package/src/service.ts +6 -0
- package/src/task/task-service.ts +2 -13
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ The daemon, CLI, and domain services behind Papyrus's graph artifact store: evid
|
|
|
9
9
|
- [Schema protocol](#schema-protocol)
|
|
10
10
|
- [Hierarchy and traversal](#hierarchy-and-traversal)
|
|
11
11
|
- [Playbooks](#playbooks)
|
|
12
|
+
- [Project scope](#project-scope)
|
|
12
13
|
- [Naming vs. ids](#naming-vs-ids)
|
|
13
14
|
- [Mutability](#mutability)
|
|
14
15
|
- [Idempotent lifecycle mutations](#idempotent-lifecycle-mutations)
|
|
@@ -75,6 +76,23 @@ papyrus playbooks invoke <playbook-id> \
|
|
|
75
76
|
--json
|
|
76
77
|
```
|
|
77
78
|
|
|
79
|
+
## Project scope
|
|
80
|
+
|
|
81
|
+
A Doc, Rule, or Playbook is either global (applies everywhere) or bound to a bounded, non-empty set of registered projects — never both, and never inferred from an accidentally empty membership. `scope` inspects an artifact's own mode and membership; `add_project`/`remove_project` mutate one membership at a time (`add_project` is idempotent for an already-present project; `remove_project` for an absent one); `replace_projects` swaps the whole set atomically; `set_global` is the only way back to global — removing an artifact's last remaining membership through `remove_project` is rejected instead of silently widening it. `assign_project` remains a documented compatibility delegate for the pre-multi-project single-root shape (replace, not add).
|
|
82
|
+
|
|
83
|
+
Listing follows the same distinction: an omitted project filter keeps the existing bounded all-artifacts search; `project_root` alone means exact membership (audit semantics — only artifacts actually bound to that project); `project_root` plus `applicable: true` means every artifact *applicable* to that project instead — global artifacts plus artifacts whose membership includes it. `pi-papyrus`'s own context injection uses `applicable` for both Rules (`rules.injectable`) and Playbooks, so a project-bound artifact never leaks into an unrelated project's prompt. Project scope governs applicability and discovery, never authorization: exact-id access works regardless of scope, the same as it always has.
|
|
84
|
+
|
|
85
|
+
A Playbook's own definition scope is a separate concern from where `playbooks.invoke` sends its generated artifacts. An unscoped or global Playbook can still be invoked with a destination `project_root`; the generated Tasks, Docs, and Rules inherit that destination, while the Playbook definition itself is untouched. A run-created Rule's injection requires both its own run to be active *and* its generated project membership to match — either alone is not enough.
|
|
86
|
+
|
|
87
|
+
The shared project catalog behind all of this (`projects.list`/`projects.resolve`/`projects.register`) is the exact one Tasks has always used, and `tasks.projects`/`tasks.resolve_project`/`tasks.register_project` remain fully working, documented compatibility delegates over it.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
papyrus docs scope <doc-id> --json
|
|
91
|
+
papyrus rules add-project <rule-id> Lector --json
|
|
92
|
+
papyrus playbooks list --project-root <root> --applicable --json
|
|
93
|
+
papyrus projects register /path/to/project --name Lector --json
|
|
94
|
+
```
|
|
95
|
+
|
|
78
96
|
## Naming vs. ids
|
|
79
97
|
|
|
80
98
|
Every agent-facing domain (tasks, docs, rules, playbooks, notes, discuss) addresses artifacts by `name` (the exact title) anywhere `id` would otherwise be required — `dependency_name`/`parent_name`/`child_name`/`root_task_name`/`depends_on_names` (tasks), `target_name` (docs link, searched across every kind), `task_name` (rules gate, discuss block/unblock), and `blocks_task_names` (discuss open). Resolution is an exact, case-insensitive, trimmed title match scoped like a plain list call; an ambiguous name's error lists the real ids, the one place disambiguation needs them. A returned result leads with name and status; id surfaces only when two artifacts in the same result share a title. `id` itself keeps working, in every tool.
|
package/package.json
CHANGED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { CommandContext } from "@stricli/core";
|
|
2
|
+
import { buildApplication, buildCommand, buildRouteMap, numberParser } from "@stricli/core";
|
|
3
|
+
import type { PapyrusClient } from "../client.ts";
|
|
4
|
+
import { runStricliToString } from "./stricli-run.ts";
|
|
5
|
+
|
|
6
|
+
type ProjectsClient = Pick<PapyrusClient, "call">;
|
|
7
|
+
|
|
8
|
+
interface ProjectsContext extends CommandContext {
|
|
9
|
+
readonly client: ProjectsClient;
|
|
10
|
+
readonly json: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseStringArray(value: string): string[] {
|
|
14
|
+
const parsed = JSON.parse(value) as unknown;
|
|
15
|
+
if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) throw new Error("value must be a JSON string array");
|
|
16
|
+
return parsed as string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface CliProject {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
aliases: string[];
|
|
23
|
+
projectRoot: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function render(this: ProjectsContext, result: unknown, human: string): void {
|
|
27
|
+
this.process.stdout.write(this.json ? JSON.stringify(result) : human);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const listCommand = buildCommand({
|
|
31
|
+
func: async function (this: ProjectsContext, flags: { query?: string; limit?: number }) {
|
|
32
|
+
const projects = await this.client.call<Record<string, unknown>, CliProject[]>("projects.list", {
|
|
33
|
+
query: flags.query,
|
|
34
|
+
limit: flags.limit,
|
|
35
|
+
});
|
|
36
|
+
render.call(
|
|
37
|
+
this,
|
|
38
|
+
projects,
|
|
39
|
+
projects.length === 0 ? "No registered projects." : projects.map((project) => `${project.name} — ${project.projectRoot}`).join("\n"),
|
|
40
|
+
);
|
|
41
|
+
},
|
|
42
|
+
parameters: {
|
|
43
|
+
flags: {
|
|
44
|
+
query: { brief: "Filter by project name, alias, or root", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
45
|
+
limit: { brief: "Maximum results", kind: "parsed", parse: numberParser, placeholder: "n", optional: true },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
docs: { brief: "List registered projects (shared by Tasks, Docs, Rules, and Playbooks)" },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const resolveCommand = buildCommand({
|
|
52
|
+
func: async function (this: ProjectsContext, _flags: Record<string, never>, reference: string) {
|
|
53
|
+
const project = await this.client.call<Record<string, unknown>, CliProject>("projects.resolve", { reference });
|
|
54
|
+
render.call(this, project, `${project.name} — ${project.projectRoot}`);
|
|
55
|
+
},
|
|
56
|
+
parameters: {
|
|
57
|
+
flags: {},
|
|
58
|
+
positional: { kind: "tuple", parameters: [{ brief: "Project id, name, alias, or root", parse: String, placeholder: "reference" }] },
|
|
59
|
+
},
|
|
60
|
+
docs: { brief: "Resolve a project reference to its canonical identity" },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const registerCommand = buildCommand({
|
|
64
|
+
func: async function (this: ProjectsContext, flags: { name?: string; aliasesJson?: string[]; existingId?: string }, projectRoot: string) {
|
|
65
|
+
const registered = await this.client.call<Record<string, unknown>, CliProject>("projects.register", {
|
|
66
|
+
project_root: projectRoot,
|
|
67
|
+
name: flags.name,
|
|
68
|
+
aliases: flags.aliasesJson,
|
|
69
|
+
existing_id: flags.existingId,
|
|
70
|
+
});
|
|
71
|
+
render.call(this, registered, `${registered.name} — ${registered.projectRoot}`);
|
|
72
|
+
},
|
|
73
|
+
parameters: {
|
|
74
|
+
flags: {
|
|
75
|
+
name: { brief: "Stable project display name", kind: "parsed", parse: String, placeholder: "name", optional: true },
|
|
76
|
+
aliasesJson: { brief: "JSON string array of aliases", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
|
|
77
|
+
existingId: {
|
|
78
|
+
brief: "Existing project id when renaming or moving",
|
|
79
|
+
kind: "parsed",
|
|
80
|
+
parse: String,
|
|
81
|
+
placeholder: "id",
|
|
82
|
+
optional: true,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
positional: { kind: "tuple", parameters: [{ brief: "Project root path", parse: String, placeholder: "project-root" }] },
|
|
86
|
+
},
|
|
87
|
+
docs: { brief: "Register a new project, or rename/move an existing one" },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const app = buildApplication(
|
|
91
|
+
buildRouteMap({
|
|
92
|
+
routes: { list: listCommand, resolve: resolveCommand, register: registerCommand },
|
|
93
|
+
docs: {
|
|
94
|
+
brief:
|
|
95
|
+
"Shared project catalog operations (compatibility delegates: tasks projects/resolve-project/register-project do the identical thing)",
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
{ name: "projects", scanner: { caseStyle: "allow-kebab-for-camel" } },
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
export async function runProjectsCli(args: string[], client: ProjectsClient): Promise<string> {
|
|
102
|
+
const json = args.includes("--json");
|
|
103
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
104
|
+
return runStricliToString(app, positional, { client, json });
|
|
105
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { runLogCli } from "./cli/log-command.ts";
|
|
|
14
14
|
import { runMigrationCli } from "./cli/migration-command.ts";
|
|
15
15
|
import { runNoteCli } from "./cli/note-command.ts";
|
|
16
16
|
import { runPlaybooksCli } from "./cli/playbooks-command.ts";
|
|
17
|
+
import { runProjectsCli } from "./cli/projects-command.ts";
|
|
17
18
|
import { runRulesCli } from "./cli/rules-command.ts";
|
|
18
19
|
import { runSessionIdentityCli } from "./cli/session-identity-command.ts";
|
|
19
20
|
import { runTaskCli } from "./cli/task-command.ts";
|
|
@@ -359,6 +360,7 @@ export {
|
|
|
359
360
|
runLogCli,
|
|
360
361
|
runNoteCli,
|
|
361
362
|
runPlaybooksCli,
|
|
363
|
+
runProjectsCli,
|
|
362
364
|
runRulesCli,
|
|
363
365
|
runSessionIdentityCli,
|
|
364
366
|
runTaskCli,
|
|
@@ -380,6 +382,11 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
380
382
|
console.log(await runPlaybooksCli(args.slice(1), client));
|
|
381
383
|
return;
|
|
382
384
|
}
|
|
385
|
+
if (command === "projects") {
|
|
386
|
+
const client = await connectPapyrusClient();
|
|
387
|
+
console.log(await runProjectsCli(args.slice(1), client));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
383
390
|
if (command === "notes") {
|
|
384
391
|
const client = await connectPapyrusClient();
|
|
385
392
|
console.log(await runNoteCli(args.slice(1), client));
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { TASK_PROJECT_ALIAS_MAX_COUNT, TASK_PROJECT_NAME_MAX_LENGTH } from "../constants.ts";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* A registered project identity, shared across every artifact kind (Tasks, Docs, Rules,
|
|
3
5
|
* Playbooks) rather than owned by Tasks alone -- extracted so a Doc/Rule/Playbook can resolve
|
|
@@ -20,6 +22,29 @@ export interface RegisterProjectInput {
|
|
|
20
22
|
existingId?: string;
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
/**
|
|
26
|
+
* The one bounded schema vocabulary for a registered project name/alias set -- shared by Tasks'
|
|
27
|
+
* own registerProject and the kind-neutral projects.register operation, rather than each domain
|
|
28
|
+
* enforcing a slightly different bound. Reuses constants.ts's existing TASK_PROJECT_* bounds
|
|
29
|
+
* (unchanged names -- the underlying catalog is Tasks' own historical one, just no longer
|
|
30
|
+
* Tasks-exclusive) rather than introducing a second, parallel set of the same numbers.
|
|
31
|
+
* Store-level registerProject (SQLiteProjectRegistryStore) does not itself validate
|
|
32
|
+
* length/count, so every caller must go through this first.
|
|
33
|
+
*/
|
|
34
|
+
export function assertRegisterProjectInputBounds(name: string | undefined, aliases: string[] | undefined): void {
|
|
35
|
+
if (name !== undefined && (name.trim().length === 0 || name.length > TASK_PROJECT_NAME_MAX_LENGTH)) {
|
|
36
|
+
throw new Error(`project name must be between 1 and ${TASK_PROJECT_NAME_MAX_LENGTH} characters`);
|
|
37
|
+
}
|
|
38
|
+
if ((aliases?.length ?? 0) > TASK_PROJECT_ALIAS_MAX_COUNT) {
|
|
39
|
+
throw new Error(`project aliases cannot exceed ${TASK_PROJECT_ALIAS_MAX_COUNT} entries`);
|
|
40
|
+
}
|
|
41
|
+
for (const alias of aliases ?? []) {
|
|
42
|
+
if (alias.trim().length === 0 || alias.length > TASK_PROJECT_NAME_MAX_LENGTH) {
|
|
43
|
+
throw new Error(`each project alias must be between 1 and ${TASK_PROJECT_NAME_MAX_LENGTH} characters`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
23
48
|
export class ProjectNotFoundError extends Error {}
|
|
24
49
|
export class ProjectAmbiguousError extends Error {}
|
|
25
50
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared project catalog projected as a real VehicleRegistry: one VehicleOperation per real
|
|
3
|
+
* action, fronting the same ProjectRegistryStore every Docs/Rules/Playbooks scope operation
|
|
4
|
+
* already resolves against. tasks.projects/tasks.resolve_project/tasks.register_project remain
|
|
5
|
+
* unchanged, documented compatibility delegates -- see handlers/tasks.ts.
|
|
6
|
+
*/
|
|
7
|
+
import type { VehicleRegistry } from "@danypops/vehicle-server";
|
|
8
|
+
import { projectsOperations } from "../modules/projects.ts";
|
|
9
|
+
import type { ProjectRegistryStore } from "../ports/project-registry-store.ts";
|
|
10
|
+
import { createOperationDefiner, numberProp, stringProp } from "./shared.ts";
|
|
11
|
+
|
|
12
|
+
const OWNER = "projects";
|
|
13
|
+
|
|
14
|
+
export function registerProjectsVehicleOperations(registry: VehicleRegistry, projectRegistry: ProjectRegistryStore): void {
|
|
15
|
+
const moduleOperations = new Map(projectsOperations(projectRegistry).map((op) => [op.name, op]));
|
|
16
|
+
const call = (name: string, input: Record<string, unknown>): unknown => moduleOperations.get(name)!.execute(input);
|
|
17
|
+
const define = createOperationDefiner(registry, OWNER, "projects", ["projects:read", "projects:write"], call);
|
|
18
|
+
|
|
19
|
+
define(
|
|
20
|
+
"list",
|
|
21
|
+
"Lists registered projects (shared by Tasks, Docs, Rules, and Playbooks), optionally filtered by a name/alias/root substring. Compatibility delegate: tasks.projects does the identical thing.",
|
|
22
|
+
"read",
|
|
23
|
+
{ query: stringProp, limit: numberProp },
|
|
24
|
+
[],
|
|
25
|
+
(input) => input,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
define(
|
|
29
|
+
"resolve",
|
|
30
|
+
"Resolves an exact project reference (id, name, alias, or registered root) to its full identity, failing closed with bounded candidates when unknown or ambiguous. Compatibility delegate: tasks.resolve_project does the identical thing.",
|
|
31
|
+
"read",
|
|
32
|
+
{ reference: stringProp },
|
|
33
|
+
["reference"],
|
|
34
|
+
(input) => input,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
define(
|
|
38
|
+
"register",
|
|
39
|
+
"Registers a new project, or renames/moves an existing one when project_root (or existing_id) already resolves to one. Compatibility delegate: tasks.register_project does the identical thing.",
|
|
40
|
+
"local-write",
|
|
41
|
+
{ project_root: stringProp, name: stringProp, aliases: { type: "array" } as unknown as { type: string }, existing_id: stringProp },
|
|
42
|
+
["project_root"],
|
|
43
|
+
(input) => input,
|
|
44
|
+
);
|
|
45
|
+
}
|
package/src/handlers/registry.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { registerDiscussVehicleOperations } from "./discuss.ts";
|
|
|
22
22
|
import { registerDocsVehicleOperations } from "./docs.ts";
|
|
23
23
|
import { registerNotesVehicleOperations } from "./notes.ts";
|
|
24
24
|
import { registerPlaybooksVehicleOperations } from "./playbooks.ts";
|
|
25
|
+
import { registerProjectsVehicleOperations } from "./projects.ts";
|
|
25
26
|
import { registerRulesVehicleOperations } from "./rules.ts";
|
|
26
27
|
import { registerTasksVehicleOperations } from "./tasks.ts";
|
|
27
28
|
|
|
@@ -62,6 +63,7 @@ export function createPapyrusVehicleRegistry(deps: PapyrusVehicleDeps): VehicleR
|
|
|
62
63
|
projectRegistry: deps.projectRegistry,
|
|
63
64
|
});
|
|
64
65
|
registerTasksVehicleOperations(registry, { tasks: deps.tasks, artifacts: deps.artifacts, sessionIdentity: deps.sessionIdentity });
|
|
66
|
+
registerProjectsVehicleOperations(registry, deps.projectRegistry);
|
|
65
67
|
registerDiscussVehicleOperations(registry, deps.discussions, deps.artifacts);
|
|
66
68
|
registerArtifactTrashOperations(registry, deps.artifacts);
|
|
67
69
|
return registry;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* modules/projects.ts — the shared project catalog as its own Papyrus-native registered module,
|
|
3
|
+
* fronting the same ProjectRegistryStore Docs/Rules/Playbooks scope operations and Tasks'
|
|
4
|
+
* project catalog already share underneath (see ports/project-registry-store.ts). Tasks'
|
|
5
|
+
* own tasks.projects/tasks.resolve_project/tasks.register_project remain fully working,
|
|
6
|
+
* documented compatibility delegates -- this module gives every other domain (and any caller
|
|
7
|
+
* with no reason to go through tasks.*) the identical operations under a kind-neutral name.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { TASK_PROJECT_LIST_MAX_RESULTS } from "../constants.ts";
|
|
11
|
+
import { assertRegisterProjectInputBounds, resolveProjectReference } from "../domain/project-registry.ts";
|
|
12
|
+
import { normalizeProjectRoot } from "../domain/task-scope.ts";
|
|
13
|
+
import type { OperationDefinition } from "../module-registry.ts";
|
|
14
|
+
import type { ProjectRegistryStore } from "../ports/project-registry-store.ts";
|
|
15
|
+
import { type OperationInput, optionalNumber, optionalString, optionalStringArray, string } from "./operation-input.ts";
|
|
16
|
+
|
|
17
|
+
const MODULE_ID = "projects";
|
|
18
|
+
|
|
19
|
+
export const PROJECTS_OPERATION_NAMES = ["projects.list", "projects.resolve", "projects.register"] as const;
|
|
20
|
+
|
|
21
|
+
export function projectsOperations(registry: ProjectRegistryStore): OperationDefinition[] {
|
|
22
|
+
const define = <Input, Output>(name: string, execute: (input: Input) => Output): OperationDefinition<Input, Output> => ({
|
|
23
|
+
name,
|
|
24
|
+
moduleId: MODULE_ID,
|
|
25
|
+
execute,
|
|
26
|
+
});
|
|
27
|
+
return [
|
|
28
|
+
define("projects.list", (input: OperationInput) => {
|
|
29
|
+
const limit = optionalNumber(input, "limit") ?? 20;
|
|
30
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > TASK_PROJECT_LIST_MAX_RESULTS) {
|
|
31
|
+
throw new Error(`project list limit must be between 1 and ${TASK_PROJECT_LIST_MAX_RESULTS}`);
|
|
32
|
+
}
|
|
33
|
+
return registry.projects(optionalString(input, "query"), limit);
|
|
34
|
+
}),
|
|
35
|
+
define("projects.resolve", (input: OperationInput) => resolveProjectReference(registry, string(input, "reference"))),
|
|
36
|
+
define("projects.register", (input: OperationInput) => {
|
|
37
|
+
const name = optionalString(input, "name");
|
|
38
|
+
const aliases = optionalStringArray(input, "aliases");
|
|
39
|
+
assertRegisterProjectInputBounds(name, aliases);
|
|
40
|
+
return registry.registerProject({
|
|
41
|
+
projectRoot: normalizeProjectRoot(string(input, "project_root")),
|
|
42
|
+
name,
|
|
43
|
+
aliases,
|
|
44
|
+
existingId: optionalString(input, "existing_id"),
|
|
45
|
+
});
|
|
46
|
+
}),
|
|
47
|
+
];
|
|
48
|
+
}
|
package/src/service.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { LOGS_OPERATION_NAMES, logsOperations } from "./modules/logs.ts";
|
|
|
28
28
|
import { NOTES_OPERATION_NAMES, notesOperations } from "./modules/notes.ts";
|
|
29
29
|
import { type OperationInput, optionalNumber, optionalString, string } from "./modules/operation-input.ts";
|
|
30
30
|
import { PLAYBOOKS_OPERATION_NAMES, playbooksOperations } from "./modules/playbooks.ts";
|
|
31
|
+
import { PROJECTS_OPERATION_NAMES, projectsOperations } from "./modules/projects.ts";
|
|
31
32
|
import { RULES_OPERATION_NAMES, rulesOperations } from "./modules/rules.ts";
|
|
32
33
|
import { SESSION_IDENTITY_OPERATION_NAMES, sessionIdentityOperations } from "./modules/session-identity.ts";
|
|
33
34
|
import { TASKS_OPERATION_NAMES, tasksOperations } from "./modules/tasks.ts";
|
|
@@ -98,6 +99,7 @@ export const EXPECTED_OPERATION_NAMES = [
|
|
|
98
99
|
...NOTES_OPERATION_NAMES,
|
|
99
100
|
...RULES_OPERATION_NAMES,
|
|
100
101
|
...PLAYBOOKS_OPERATION_NAMES,
|
|
102
|
+
...PROJECTS_OPERATION_NAMES,
|
|
101
103
|
...GRAPH_PROJECTION_OPERATION_NAMES,
|
|
102
104
|
...LOGS_OPERATION_NAMES,
|
|
103
105
|
...SESSION_IDENTITY_OPERATION_NAMES,
|
|
@@ -451,6 +453,9 @@ function handlers(
|
|
|
451
453
|
"playbooks.uncontain": forwardToModule("playbooks.uncontain"),
|
|
452
454
|
"playbooks.depend": forwardToModule("playbooks.depend"),
|
|
453
455
|
"playbooks.undepend": forwardToModule("playbooks.undepend"),
|
|
456
|
+
"projects.list": forwardToModule("projects.list"),
|
|
457
|
+
"projects.resolve": forwardToModule("projects.resolve"),
|
|
458
|
+
"projects.register": forwardToModule("projects.register"),
|
|
454
459
|
"graph_projection.apply": forwardToModule("graph_projection.apply"),
|
|
455
460
|
"graph_projection.checkpoint": forwardToModule("graph_projection.checkpoint"),
|
|
456
461
|
"logs.append": forwardToModule("logs.append"),
|
|
@@ -513,6 +518,7 @@ export function createPapyrusService(path: string): PapyrusService {
|
|
|
513
518
|
moduleRegistry.registerAll(
|
|
514
519
|
playbooksOperations({ artifacts, events, scopes, artifactScopes, tasks, sessionIdentity, registry: projectRegistry }),
|
|
515
520
|
);
|
|
521
|
+
moduleRegistry.registerAll(projectsOperations(projectRegistry));
|
|
516
522
|
moduleRegistry.registerAll(graphProjectionOperations(artifacts, projections, authority));
|
|
517
523
|
const registry = handlers(artifacts, gates, tasks, notes, events, scopes, artifactScopes, () => migrateDb(db), moduleRegistry, authority);
|
|
518
524
|
const state = (): SchemaState => {
|
package/src/task/task-service.ts
CHANGED
|
@@ -14,15 +14,14 @@ import {
|
|
|
14
14
|
TASK_LABEL_MAX_LENGTH,
|
|
15
15
|
TASK_MUTATION_IDEMPOTENCY_KEY_MAX_LENGTH,
|
|
16
16
|
TASK_MUTATION_IDEMPOTENCY_RETENTION_MS,
|
|
17
|
-
TASK_PROJECT_ALIAS_MAX_COUNT,
|
|
18
17
|
TASK_PROJECT_LIST_MAX_RESULTS,
|
|
19
|
-
TASK_PROJECT_NAME_MAX_LENGTH,
|
|
20
18
|
TASK_SCOPE_MAX_TASKS,
|
|
21
19
|
TASK_TITLE_MAX_LENGTH,
|
|
22
20
|
} from "../constants.ts";
|
|
23
21
|
import { type Checklist, checklistEntries, type ProofReference, validateChecklist } from "../domain/checklist.ts";
|
|
24
22
|
import { DISCUSSION_SUBTYPE, isDiscussionArtifact, readDiscussionExtra } from "../domain/discussion.ts";
|
|
25
23
|
import { type Gate, type GateResult, validateGates } from "../domain/gate.ts";
|
|
24
|
+
import { assertRegisterProjectInputBounds } from "../domain/project-registry.ts";
|
|
26
25
|
import type {
|
|
27
26
|
AppendTaskEvent,
|
|
28
27
|
TaskEventContext,
|
|
@@ -549,17 +548,7 @@ export class Tasks {
|
|
|
549
548
|
registerProject(input: RegisterTaskProjectInput, existingReference?: string): TaskProject {
|
|
550
549
|
const projectRoot = normalizeProjectRoot(input.projectRoot);
|
|
551
550
|
const name = input.name?.trim();
|
|
552
|
-
|
|
553
|
-
throw new Error(`project name must be between 1 and ${TASK_PROJECT_NAME_MAX_LENGTH} characters`);
|
|
554
|
-
}
|
|
555
|
-
if ((input.aliases?.length ?? 0) > TASK_PROJECT_ALIAS_MAX_COUNT) {
|
|
556
|
-
throw new Error(`project aliases cannot exceed ${TASK_PROJECT_ALIAS_MAX_COUNT} entries`);
|
|
557
|
-
}
|
|
558
|
-
for (const alias of input.aliases ?? []) {
|
|
559
|
-
if (alias.trim().length === 0 || alias.length > TASK_PROJECT_NAME_MAX_LENGTH) {
|
|
560
|
-
throw new Error(`each project alias must be between 1 and ${TASK_PROJECT_NAME_MAX_LENGTH} characters`);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
551
|
+
assertRegisterProjectInputBounds(name, input.aliases);
|
|
563
552
|
const existingId = existingReference ? this.resolveProject(existingReference).id : input.existingId;
|
|
564
553
|
return this.scopes.registerProject({ projectRoot, ...(name ? { name } : {}), aliases: input.aliases, existingId });
|
|
565
554
|
}
|